I'm trying to get up and running with an NSMetadataQueryDidUpdateNotification on an OS X app, to alert me when a file in my iCloud ubiquity container is updated. I've been doing a lot of research (including reading other Stack answers like this, this, this, and this), but I still don't have it quite right, it seems.
I've got a "CloudDocument" object subclassed from NSDocument, which includes this code in the H:
#property (nonatomic, strong) NSMetadataQuery *alertQuery;
and this is the M file:
#synthesize alertQuery;
-(id)init {
self = [super init];
if (self) {
if (alertQuery) {
[alertQuery stopQuery];
} else {
alertQuery = [[NSMetadataQuery alloc]init];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(queryDidUpdate:) name:NSMetadataQueryDidUpdateNotification object:nil];
NSLog(#"Notification created");
[alertQuery startQuery];
}
return self;
}
-(void)queryDidUpdate:(NSNotification *)notification {
NSLog(#"Something changed!!!");
}
According to my best understanding, that should stop a pre-existing query if one is running, set up a notification for changes to the ubiquity container, and then start the query so it will monitor changes from here on out.
Except, clearly that's not the case because I get Notification created in the log on launch but never Something changed!!! when I change the iCloud document.
Can anyone tell me what I'm missing? And if you're extra-super-sauce awesome, you'll help me out with some code samples and/or tutorials?
Edit: If it matters/helps, there is only one file in my ubiquity container being synced around. It's called "notes", so I access it using the URL result from:
+(NSURL *)notesURL {
NSURL *url = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
return [url URLByAppendingPathComponent:kAllNotes];
}
where "kAllNotes" is set with #define kAllNotes #"notes".
EDIT #2: There have been a lot of updates to my code through my conversation with Daij-Djan, so here is my updated code:
#synthesize alertQuery;
-(id)init {
self = [super init];
if (self) {
alertQuery = [[NSMetadataQuery alloc] init];
if (alertQuery) {
[alertQuery setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
NSString *STEDocFilenameExtension = #"*";
NSString* filePattern = [NSString stringWithFormat:#"*.%#", STEDocFilenameExtension];
[alertQuery setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE %#", NSMetadataItemFSNameKey, filePattern]];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(queryDidUpdate:) name:NSMetadataQueryDidFinishGatheringNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(queryDidUpdate:) name:NSMetadataQueryDidUpdateNotification object:nil];
NSLog(#"Notification created");
[alertQuery startQuery];
}
return self;
}
-(void)queryDidUpdate:(NSNotification *)notification {
[alertQuery disableUpdates];
NSLog(#"Something changed!!!");
[alertQuery enableUpdates];
}
How do you save your document - what url do you give it? Unless you give it an extension yourself, it won't automatically be given one - so your *.* pattern will never match a file that does not have an extension. Try * as the pattern and see what happens.
Also, it helps to log what is happening within queryDidUpdate, until you've worked out exactly what's going on :
Try something like:
-(void)queryDidUpdate:(NSNotification *)notification {
[alertQuery disableUpdates];
NSLog(#"Something changed!!!");
// Look at each element returned by the search
// - note it returns the entire list each time this method is called, NOT just the changes
int resultCount = [alertQuery resultCount];
for (int i = 0; i < resultCount; i++) {
NSMetadataItem *item = [alertQuery resultAtIndex:i];
[self logAllCloudStorageKeysForMetadataItem:item];
}
[alertQuery enableUpdates];
}
- (void)logAllCloudStorageKeysForMetadataItem:(NSMetadataItem *)item
{
NSNumber *isUbiquitous = [item valueForAttribute:NSMetadataItemIsUbiquitousKey];
NSNumber *hasUnresolvedConflicts = [item valueForAttribute:NSMetadataUbiquitousItemHasUnresolvedConflictsKey];
NSNumber *isDownloaded = [item valueForAttribute:NSMetadataUbiquitousItemIsDownloadedKey];
NSNumber *isDownloading = [item valueForAttribute:NSMetadataUbiquitousItemIsDownloadingKey];
NSNumber *isUploaded = [item valueForAttribute:NSMetadataUbiquitousItemIsUploadedKey];
NSNumber *isUploading = [item valueForAttribute:NSMetadataUbiquitousItemIsUploadingKey];
NSNumber *percentDownloaded = [item valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey];
NSNumber *percentUploaded = [item valueForAttribute:NSMetadataUbiquitousItemPercentUploadedKey];
NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
BOOL documentExists = [[NSFileManager defaultManager] fileExistsAtPath:[url path]];
NSLog(#"isUbiquitous:%# hasUnresolvedConflicts:%# isDownloaded:%# isDownloading:%# isUploaded:%# isUploading:%# %%downloaded:%# %%uploaded:%# documentExists:%i - %#", isUbiquitous, hasUnresolvedConflicts, isDownloaded, isDownloading, isUploaded, isUploading, percentDownloaded, percentUploaded, documentExists, url);
}
you never allocate your alertQuery....
somewhere you need to alloc,init a NSMetaDataQuery for it
example
NSMetadataQuery* aQuery = [[NSMetadataQuery alloc] init];
if (aQuery) {
// Search the Documents subdirectory only.
[aQuery setSearchScopes:[NSArray
arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
// Add a predicate for finding the documents.
NSString* filePattern = [NSString stringWithFormat:#"*"];
[aQuery setPredicate:[NSPredicate predicateWithFormat:#"%K LIKE %#",
NSMetadataItemFSNameKey, filePattern]];
// Register for the metadata query notifications.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(processFiles:)
name:NSMetadataQueryDidFinishGatheringNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(processFiles:)
name:NSMetadataQueryDidUpdateNotification
object:nil];
// Start the query and let it run.
[aQuery startQuery];
}
processFiles method
== your queryDidUpdate
- processFiles(NSNotification*)note {
[aQuery disableUpdates];
.....
[aQuery enableUpdates];
}
NOTE: disable and reenable search there!
see:
http://developer.apple.com/library/ios/#documentation/General/Conceptual/iCloud101/SearchingforiCloudDocuments/SearchingforiCloudDocuments.html
Related
When a group has been removed –groupForURL:resultBlock:failureBlock: will not be able to find the group anymore. Now you might say you must compare the NSURL you received from the notification with the NSURL you get when using ALAssetsGroupPropertyURL on the current group. However when the current group has a filter on it, the NSURL will be appended with &filter= and the NSURLs won't match.
So if you have a current group of type ALAssetsGroup and you receive a notification from ALAssetsLibraryChangedNotification. How do you check if the current group is the one that got removed?
My code so far:
- (void)viewDidLoad
{
[super viewDidLoad];
self.collectionView.alwaysBounceVertical = YES;
self.collectionView.backgroundColor = [UIColor whiteColor];
self.collectionView.opaque = YES;
self.collectionView.contentInset = UIEdgeInsetsMake(9, 0, 0, 0);
[self.collectionView registerClass:[CMPhotoCollectionViewCell class] forCellWithReuseIdentifier:#"PhotoCell"];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(assetsLibraryDidChange:) name:ALAssetsLibraryChangedNotification object:nil];
self.title = [self.album.group valueForProperty:ALAssetsGroupPropertyName];
if (!self.assets)
_assets = [NSMutableArray new];
else
[self.assets removeAllObjects];
ALAssetsGroupEnumerationResultsBlock assetsEnumerationBlock = ^(ALAsset *result, NSUInteger index, BOOL *stop)
{
if (result) [self.assets addObject:result];
};
[self.album.group enumerateAssetsUsingBlock:assetsEnumerationBlock];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.collectionView reloadData];
}
#pragma mark -
#pragma mark Assets Library
- (void)assetsLibraryDidChange:(NSNotification *)notification
{
if (notification.userInfo[ALAssetLibraryDeletedAssetGroupsKey])
{
NSLog (#"Album removed: %#", notification.userInfo[ALAssetLibraryDeletedAssetGroupsKey]);
NSLog (#"This album: %#", self.album.group);
NSLog (#"%#", [self.album.group valueForProperty:ALAssetsGroupPropertyURL]);
// I get a NSSet with NSURLs of groups that have been removed.
// However the NSURL seems to be always without filter, if my current item
// has a filter on it, the URLS don't match.
}
}
#pragma mark - UICollectionViewDelegate
- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section
{
return self.assets.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CMPhotoCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"PhotoCell" forIndexPath:indexPath];
ALAsset *asset = self.assets[indexPath.row];
UIImage *thumbnail = [UIImage imageWithCGImage:asset.thumbnail];
cell.imageView.image = thumbnail;
return cell;
}
I am trying to monitor file changes in local and iCloud directories and have implemented the NSFilePresenter protocol methods but the only method that gets called is presentedItemAtURL.
Am I correct in assuming that I should be able to monitor a local or an iCloud directory and get notified any time any process adds, modifies or deletes a file in the directory.
Here is the basic code for the OS X App:
- (void)awakeFromNib {
_presentedItemURL = myDocumentsDirectoryURL;
_presentedItemOperationQueue = [[NSOperationQueue alloc] init];
[_presentedItemOperationQueue setMaxConcurrentOperationCount: 1];
_fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
}
- (NSURL*) presentedItemURL {
FLOG(#" called %#", _presentedItemURL);
return _presentedItemURL;
}
- (NSOperationQueue*) presentedItemOperationQueue {
FLOG(#" called");
return _presentedItemOperationQueue;
}
- (void)presentedItemDidChange {
FLOG(#" called");
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData];
});
}
-(void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler
{ FLOG(#" called");
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self reloadData];
}];
completionHandler(nil);
}
-(void)presentedSubitemDidChangeAtURL:(NSURL *)url {
FLOG(#" called");
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData];
});
}
-(void)presentedSubitemDidAppearAtURL:(NSURL *)url {
FLOG(#" called");
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData];
});
}
Long time ago, I know, but perhaps this will still help. NSFilePresenter will only notify you about changes made by another process that makes changes to a directory or file USING AN NSFileCoordinator. If another process (eg: iTunes file sharing) makes changes without an NSFileCoordinator, you won't be notified.
This is in no way my final implementation and I will edit/update as I improve. But since there is nil examples on how to do this, i figured i'd share something that works!!! That's right, it works. I am able to read the file in my app, and at the same time make a change in textedit and the changes propagate to my app. Hope this helps bud.
PBDocument.h
#interface PBDocument : NSObject <NSFilePresenter>
#property (nonatomic, strong) NSTextView *textView;
#pragma mark - NSFilePresenter properties
#property (readonly) NSURL *presentedItemURL;
#property (readonly) NSOperationQueue *presentedItemOperationQueue;
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError *__autoreleasing *)outError textView:(NSTextView*)textView;
#end
PBDocument.m
#interface PBDocument ()
#property (readwrite) NSURL *presentedItemURL;
#property (readwrite) NSOperationQueue *presentedItemOperationQueue;
#property (readwrite) NSFileCoordinator *fileCoordinator;
#end
#implementation PBDocument
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError *__autoreleasing *)outError textView:(NSTextView*)textView {
self = [super init];
if (self) {
_textView = textView;
_presentedItemURL = url;
_presentedItemOperationQueue = [NSOperationQueue mainQueue];
[NSFileCoordinator addFilePresenter:self];
_fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
[self readWithCoordination];
}
return self;
}
- (void)readWithCoordination {
NSError *error = nil;
[self.fileCoordinator coordinateReadingItemAtURL:_presentedItemURL options:NSFileCoordinatorReadingWithoutChanges error:&error byAccessor:^(NSURL *newURL) {
NSLog(#"Coordinating Read");
NSError *error = nil;
NSFileWrapper *wrapper = [[NSFileWrapper alloc] initWithURL:newURL options:0 error:&error];
if (!error) {
[self readFromFileWrapper:wrapper ofType:[self.presentedItemURL pathExtension] error:&error];
}
if (error) #throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:#"%#", error] userInfo:nil];
}];
if (error) #throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:#"%#", error] userInfo:nil];
}
- (void)presentedItemDidChange {
[self readWithCoordination];
}
#end
If it's any help to anyone this is the approach (FSEvents) I ended up using recently for a file sync solution and it seems to work for any file system. I have not done any research recently on NSFileCoordinator to see whether this is better worse or what the use cases are as a comparison.
I also did not test every use case so your mileage may vary.
https://github.com/eonil/FSEvents
I've an aNotification with a userInfo I'd like to call in another method with a different format.
Is there a way to retrieve a string from the [aNotification userInfo] and modify it?
userInfo is something formatted like this:
{
action = "the string I'd like to use";
}
I did like this (and it's almost working) but I feel there is a better way to do the same.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(playClicked:) name:kNotificationActionTapped object:nil];
..................
-(void)playClicked:(NSNotification *)aNotification {
NSLog(#"Notification=%#", [aNotification userInfo]);
SBJsonWriter *jsonWriter = [[SBJsonWriter alloc] init];
NSString *jsonString = [jsonWriter stringWithObject:[aNotification userInfo]];
[jsonWriter release];
NSString * string1 = [jsonString substringFromIndex:11];
NSString * string2 = [string1 substringToIndex:[watch length] -2];
NSLog(#"string=%#", string1);
NSLog(#"string=%#", string2);
}
userInfo is an NSDictionary. Use standard NSDictionary methods.
NSString *aString = [aNotification.userInfo objectForKey:#"action"];
I have written a tab based application in Xcode/RestKit and am attempting to use the RKReachabilityObserver to determine Internet connectivity on the device.
Ideally I'd like to have a single reachability variable throughout my application (if this is possible) but currently my implementation is as per the code below and does not work very well when replicated over my 4 tabs.
If anybody has any suggestions of a better way to do this, I'd really appreciate your comments.
View.h
#property (nonatomic, retain) RKReachabilityObserver *observer;
View.m
#interface AppViewController()
{
RKReachabilityObserver *_observer;
}
#property (nonatomic) BOOL networkIsAvailable;
#synthesize observer = _observer;
-(id)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super initWithCoder:aDecoder])) {
self.observer = [[RKReachabilityObserver alloc] initWithHost:#"mydomain"];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reachabilityChanged:)
name:RKReachabilityDidChangeNotification
object:_observer];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// determine network availability
if (! [_observer isReachabilityDetermined]) {
_networkIsAvailable = YES;
}
else
{
_networkIsAvailable = NO;
}
_text.returnKeyType = UIReturnKeyDone;
_text.delegate = self;
}
- (void)reachabilityChanged:(NSNotification *)notification {
RKReachabilityObserver* observer = (RKReachabilityObserver *) [notification object];
if ([observer isNetworkReachable]) {
if ([observer isConnectionRequired]) {
_networkIsAvailable = YES;
NSLog(#"Reachable");
return;
}
}
else
{
_networkIsAvailable = NO;
NSLog(#"Not reachable");
}
}
then anywhere in my view, I simply do....
if (_networkIsAvailable == YES)
{...
I have implemented this over multiple views (which seems to be causing the problem.
What is the suggested approach for a multiple-view application?
The [RKClient sharedClient] singleton already has a property for that (reachabilityObserver). Feel free to use that one.
if ([[[RKClient sharedClient] reachabilityObserver] isReachabilityDetermined] && [[RKClient sharedClient] isNetworkReachable]) {
....
}
You can also subscribe to RKReachabilityObserver notifications (if you want to take any action when reachability status changes)
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reachabilityStatusChanged:)
name:RKReachabilityDidChangeNotification object:nil];
Here is some changes in RestKit 0.20 and later.
The code of reachability block should looks like:
RKObjectManager *manager = [RKObjectManager managerWithBaseURL:[RemoteTools serverUrl]];
[manager.HTTPClient setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
if (status == AFNetworkReachabilityStatusNotReachable) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"No network connection"
message:#"You must be connected to the internet to use this app."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
}
}];
My code is just play a downloaded mp4 files and register the view controller to observe the notification of end of player.
It works pretty good in not only iOS5 but also iOS4.
But I just want to know for sure that whether the call back method by NotificationCenter will be called in background thread or main thread.
(loadMoviePlayerStateChanged:(NSNotification*)notification is call back method in my code)
Do anyone know exactly about this?
- (void) playMovie:(NSURL *)fileURL {
MPMoviePlayerViewController *MPVC = [[MPMoviePlayerViewController alloc] initWithContentURL:fileURL];
self.mMPVC = MPVC;
self.mMPVC.moviePlayer.controlStyle = MPMovieControlStyleFullscreen;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(loadMoviePlayerStateChanged:)
name:MPMoviePlayerLoadStateDidChangeNotification
object:self.mMPVC.moviePlayer];
[MPVC.moviePlayer prepareToPlay];
[MPVC release];
}
- (void) loadMoviePlayerStateChanged:(NSNotification*)notification {
int loadState = self.mMPVC.moviePlayer.loadState;
if(loadState & MPMovieLoadStateUnknown) {
IGLog(#"The load state is not known at this time.");
return;
}
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMoviePlayerLoadStateDidChangeNotification
object:self.mMPVC.moviePlayer];
[self.mMPVC.view setFrame:self.view.bounds];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayBackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:self.mMPVC.moviePlayer];
/* if I do not use performSelectorOnMainThread method to add subview to UIViewController`s view,
the view of MPMoviePlayerViewController would not be removed from superview normally */
[self.view performSelectorOnMainThread:#selector(addSubview:)
withObject:self.mMPVC.view
waitUntilDone:YES];
[self.mMPVC.moviePlayer play];
}
- (void) moviePlayBackDidFinish:(NSNotification*)notification {
[self.mMPVC.moviePlayer stop];
[self.mMPVC.moviePlayer.view removeFromSuperview];
NSString* dstFilePath = [[_mPopupVC.mSelectedMovie decryptionFilePath] stringByAppendingPathExtension:#"mp4"];
[[NSFileManager defaultManager] removeItemAtPath:dstFilePath error:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMoviePlayerPlaybackDidFinishNotification
object:self.mMPVC.moviePlayer];
}
I got an answer for my question in apple developer site, the answer is,
"notifications safely thread themselves and perform their selectors on the main thread.
however, your problem is that you should not be adding and removing the player view like that, use the presentModalVideo methods provided as a category in your view controller class."
and I solved the problem that i had. code is below..
- (void) playMovie
{
/*
*create and initialize MPMoviePlayerViewController with specified url and retain it
*/
MPMoviePlayerViewController *MPVC = [[MPMoviePlayerViewController alloc] initWithContentURL:vodWebURL];
self.mMPVC = MPVC;
[MPVC release];
self.mMPVC.moviePlayer.controlStyle = MPMovieControlStyleFullscreen;
self.mMPVC.moviePlayer.shouldAutoplay = NO;
[self.mMPVC.moviePlayer prepareToPlay];
/*
*register movie player to NSNotificationCenter
*/
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(loadMoviePlayerStateChanged:)
name:MPMoviePlayerLoadStateDidChangeNotification
object:self.mMPVC.moviePlayer];
}
- (void) loadMoviePlayerStateChanged:(NSNotification*)notification
{
/*
*do your work for the state of the movie player
*/
int loadState = self.mMPVC.moviePlayer.loadState;
if(loadState & MPMovieLoadStateUnknown) {
NSLog(#"The load state is not known at this time.");
return;
} else if(loadState & MPMovieLoadStatePlayable) {
NSLog(#"MPMovieLoadStatePlayable : The buffer has enough data that playback can begin, but it may run out of data before playback finishes.");
} else if(loadState & MPMovieLoadStatePlaythroughOK) {
NSLog(#"MPMovieLoadStatePlaythroughOK : Enough data has been buffered for playback to continue uninterrupted.");
} else if(loadState & MPMovieLoadStateStalled) {
NSLog(#"MPMovieLoadStateStalled : The buffering of data has stalled.");
}
/*
*set frame of the view of MPMoviePlayerViewController and add it
*call play method
*/
[self.mMPVC.view setFrame:self.view.superview.bounds];
[self.view.superview addSubview:self.mMPVC.view];
[self.mMPVC.moviePlayer play];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMoviePlayerLoadStateDidChangeNotification
object:self.mMPVC.moviePlayer];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayBackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:self.mMPVC.moviePlayer];
}
- (void) moviePlayBackDidFinish:(NSNotification*)notification
{
/*
*remove the view of MPMoviePlayerViewController
*release MPMoviePlayerViewController
*/
[self.mMPVC.moviePlayer stop];
[self.mMPVC.moviePlayer.view removeFromSuperview];
self.mMPVC = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMoviePlayerPlaybackDidFinishNotification
object:self.mMPVC.moviePlayer];
}