I have a UITableview I'm keeping updated with recent items. (That is, added to my CoreData within the last 5 minutes.) I have a field in my Item entity called 'expire_date' which is a Date type. When I download a new item in the background, I add it to CoreData, setting the expire_date to NSDate() plus 5 minutes:
item.expire_date = NSDate(timeIntervalSince1970: NSDate().timeIntervalSince1970+5*60);
My setup for the NSFetchedResultsController looks like:
let fetchRequest=NSFetchRequest(entityName: "Item")
fetchRequest.predicate = NSPredicate(format: "expire_date > now()")
let sortDescriptor = NSSortDescriptor(key: "expire_date")
fetchRequest.sortDescriptors=[sortDescriptor]
myFRC=NSFetchedResultsController(fetchRequest:fetchRequest, managedObjectContext:myMOC)
do {
try myFRC!.performFetch()
} catch {
print(error)
}
The NSFetchedResultsControllerDelegate code is the standard boilerplate for this kind of thing when using a UITableView.
This all works great when starting the app: only existing items that haven't 'timed out' show up. It also works great when new items are added.
The problem is that when an item in the list times out, it doesn't get removed from the results
Anyone have any idea how to accomplish this?
I had the possibility of one or more Items being selected in the UITableView, and I didn't want to lose those selections so I couldn't use W.K.S.'s answer unfortunately. It was strictly speaking correct from the information I had given, although it didn't update the list immediately after any item expires. However, Wain gave me the spark of an idea that worked the way I wanted.
It seems the Item is re-examined by the query when that item is updated, so the trick is to update the item in question when it has expired. I added an NSTimer variable:
var timeoutTimer:NSTimer?
I added a function, addMinimumTimeout() that (re)sets the timeout timer:
func addMinimumTimeout() {
if let _timeoutTimer=timeoutTimer {
_timeoutTimer.invalidate();
timeoutTimer=nil;
}
if myFRC?.fetchedObjects?.count==0 {
return;
}
if let firstItem = myFRC?.fetchedObjects?[0] as? Item {
let timeout=(firstItem.expire_date!.timeIntervalSince1970-NSDate().timeIntervalSince1970)+1.0;
timeoutTimer=NSTimer.scheduledTimerWithTimeInterval(timeout, target: self, selector: #selector(itemTimedOut), userInfo: firstItem, repeats: false);
}
}
My itemTimedOut code looks like this:
func itemTimedOut(timer:NSTimer) {
guard let item=timer.userInfo as? Item else {
return;
}
item.expire_date! = item.expire_date!;
do {
try item.managedObjectContext!.save();
} catch {
let saveError = error as NSError;
print("Error saving: \(saveError)")
return;
}
}
Then in my viewDidLoad, right after I perform my fetch, I call:
addMinimumTimeout();
and I also add it at the end of the boilerplate controller(controller, didChangeObject, atIndexPath, forChangeType, newIndexPath) function.
This way, if there's no items in the list, there's no timeout timer created, but as soon as one is added, the timeout timer is created. When an item is removed for whatever reason, the timer is updated, and if the last one is timed out or removed, there's no timeout timer running.
Related
So I have been trying to use the MusicKit APIs for a few days now. I have been attempting to use the MPMusicPlayerApplicationController and MutableQueue APIs.
I have queue initialized already using setQueue(with: [String]) with an array of store identifiers for Apple Music songs. Then I want to allow the user to reorder the songs in the queue. I use the following code to attempt that.
let musicPlayerController = MPMusicPlayerController.applicationQueuePlayer
musicPlayerController.perform(queueTransaction: { queue in
let afterItem = queue.items.first(where: { $0.playbackStoreID == predecessorId })
let descriptor = MPMusicPlayerStoreQueueDescriptor(storeIDs: [newItemId])
queue.insert(descriptor, after: afterItem)
}) { (queue, error) in
// Completion for when items' position update
if error != nil {
print(error!)
}
}
The code above works as expected if afterItem is nil (i.e. the song is correctly inserted at the front of the queue). However, if afterItem is not nil, nothing happens. The queue stays the exact same as if no insert happened and there is no error provided in the completion handler. This problem happens regardless of whether the song being inserted is already in the queue or not.
Am I attempting modifying the queue incorrectly?
Ok, I found the solution.
If you want the queue to be mutated.
You need to return the query
let musicPlayerController = MPMusicPlayerController.applicationQueuePlayer
musicPlayerController.perform(queueTransaction: { queue in
let afterItem = queue.items.first(where: { $0.playbackStoreID == predecessorId })
let descriptor = MPMusicPlayerStoreQueueDescriptor(storeIDs: [newItemId])
//return the modification here.
return queue.insert(descriptor, after: afterItem)
}) { (queue, error) in
// Completion for when items' position update
if error != nil {
print(error!)
}
}
Here is my situation. (Using Swift 2.2)
I have a list of coordinates (CLLocation). I need to call the reverseGeocodeLocation to fetch the corresponding Place/City. If I try to loop through the elements there is a chance for some calls to fails as Apple suggest to send one call in a second. So I need to add a delay between each calls as well.
Is there any way to achieve this? Any help is appreciated.
(If we have multiple items with same lat, long we only call the api once)
This code declares a set of locations and looks them up one by one, with at least 1 second between requests:
var locations = Set<CLLocation>()
func reverseGeocodeLocation() {
guard let location = locations.popFirst() else {
geocodingDone()
return
}
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
//Do stuff here
//Dispatch the next request in 1 second
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
self.reverseGeocodeLocation()
}
}
}
func geocodingDone() {
//Put your finish logic in here
}
FYI I used the block syntax for the Timer, but that only works on iOS 10. If you are using iOS 9 or earlier just use the selector version and it works the same way.
I have a Tableview that gets data with findObjectsInBackgroundWithBlock in viewDidLoad and passes that data to a Detail View Controller no problem.
Im having trouble managing the flow of findObjectsInBackgroundWithBlock. Here is a example: I have a like button on the detail view and when pressed it increments the UILabel and displays it. It also then gets the object in Parse then increments and saves it... Everything good.
#IBAction func likeButtonPressed(sender: AnyObject) {
print("likeButtonPressed()")
// Adding the like to label
mixLike!++
var stringForCount: String = String(mixLike!)
mixLikeLabel.text = stringForCount
// Saving the like back to Parse
var query = PFQuery(className: "musicMixes")
query.whereKey("info", equalTo: mixNameLabel.text)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!, error:NSError!) -> Void in
if error == nil {
for object in objects {
//var votes = object["votes"] as! Int
let mixObject:PFObject = object as! PFObject
mixObject.incrementKey("votes", byAmount: 1)
mixObject.saveInBackgroundWithTarget(nil, selector: nil)
print("mixObjectSaved")
}
} else {
print("Error getLikeCount()")
}
print("sending Notification...")
NSNotificationCenter.defaultCenter().postNotificationName("reload", object: nil)
print("sent Notification...")
}
} // likeButtonPressed End
I also then call a NSNotification back to the Table View so the Table View can update the likes to match the users like click on the detail view (See bellow)
The NSNotification calls this function in the Table View, which removes the like array, grabs the new likes again and then reloads the Table View.
# objc func reloadTableData(notification: NSNotification){
print("Notification Recived, Removing Likes and Reloading. reloadTableData()...")
self.mixLikeArray.removeAll()
//self.stringForCountArray.removeAll()
print("Like array Data removed, getting data again...")
var query = PFQuery(className: "musicMixes")
query.orderByAscending("date")
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
if error == nil {
for object in objects {
let mixLike = object["votes"] as! Int
self.mixLikeArray.append(mixLike)
print("New mixLikeArray data is \(self.mixLikeArray)")
}
} else {
print("error getting like object")
}
}
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
}
I see three issues wrong with how this works at the moment. likeButtonPressed() Is sometimes sending the NSNotification before mixObject.saveInBackgroundWithTarget is finished. Meaning that the incremented like won't be displayed on the table view.
Secondly if I was to click like then click back to tableview swiftly the app will crash. This is because I'm guessing both likeButtonPressed() and the NSNotification function still has not been completed.
Also in # objc func reloadTableData(notification: NSNotification) once again the
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
Is being called before the findObjectsInBackgroundWithBlock is being completed? Anyway round this?
How would you suggest I can remodel this to work efficiently? Im pretty new to coding and a bit rusty with designing the best ways to do things... I know the concept behind completion handlers could I use these? I know that Parse likes to work in the background though hhhmmmm.....
to fix your reloadTableData problem, you should trigger the reload once the parse block is done executing, which means moving this line
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
inside the block
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
if error == nil {
for object in objects {
let mixLike = object["votes"] as! Int
self.mixLikeArray.append(mixLike)
print("New mixLikeArray data is \(self.mixLikeArray)")
}
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
} else {
print("error getting like object")
}
}
That will ensure that it gets triggered once parse is done updating objects. Currently its triggering before that while the block is executing. It also means that it won't reload if you get an error as you probably need to handle that differently anyway.
As for your problem of the notification happening before the saving is complete, you are calling . saveInBackgroundWithTarget but don't seem to send anything into it. You could use saveInBackgroundWithBlock and then use dispatch_group dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify inside the block to make your program wait till everything is done being saved before sending the notification.
So you would create a dispatch_group
dispatch_group_t group = dispatch_group_create();
And then call it dispatch_group_enter in the for loop through the objects
for object in objects {
dispatch_group_enter(group);
let mixObject:PFObject = object as! PFObject
.....
}
Then call dispatch_group_leave on the mixObject.saveInBackgroundWithBlock
and wrap the notification in dispatch_group_notify
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 4
NSNotificationCenter.defaultCenter().postNotificationName("reload", object: nil)
});
Something like that
It sounds more daunting than it is, here's a Ray Wenderlich tutorial to bring you up to speed on how to use it, if your not familiar
So I'm looking at the Photos API and Apple's sample code here
https://developer.apple.com/library/ios/samplecode/UsingPhotosFramework/Introduction/Intro.html
and its conversion to swift here
https://github.com/ooper-shlab/SamplePhotosApp-Swift
I have integrated the code into my project so that a collectionView is successfully updating itself form the library as I take photos. There is one quirk: Sometimes cells are blank, and it seems to be connected to stopCachingImagesForAllAssets which Apple calls each time the library is updated at the end of photoLibraryDidChange delegate method.
I can remove the line and it fixes the problem, but surely there is a reason Apple put it there in the first place? I am concerned with memory usage.
// MARK: - PHPhotoLibraryChangeObserver
func photoLibraryDidChange(changeInstance: PHChange) {
// Check if there are changes to the assets we are showing.
guard let
assetsFetchResults = self.assetsFetchResults,
collectionChanges = changeInstance.changeDetailsForFetchResult(assetsFetchResults)
else {return}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue()) {
// Get the new fetch result.
self.assetsFetchResults = collectionChanges.fetchResultAfterChanges
let collectionView = self.pictureCollectionView!
if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
// Reload the collection view if the incremental diffs are not available
collectionView.reloadData()
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
collectionView.performBatchUpdates({
if let removedIndexes = collectionChanges.removedIndexes
where removedIndexes.count > 0 {
collectionView.deleteItemsAtIndexPaths(removedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let insertedIndexes = collectionChanges.insertedIndexes
where insertedIndexes.count > 0 {
collectionView.insertItemsAtIndexPaths(insertedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let changedIndexes = collectionChanges.changedIndexes
where changedIndexes.count > 0 {
collectionView.reloadItemsAtIndexPaths(changedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
}, completion: nil)
}
self.resetCachedAssets() //perhaps prevents memory warning but causes the empty cells
}
}
//MARK: - Asset Caching
private func resetCachedAssets() {
self.imageManager?.stopCachingImagesForAllAssets()
self.previousPreheatRect = CGRectZero
}
I was having the same result.
Here's what fixed the issue for me:
Since performBatchUpdates is asynchronous, the resetCachedAssets gets executed possibly while the delete/insert/reload is happening, or even between those.
That didn't sound nice to me. So I moved the line:
self.resetCachedAssets()
to the first line of the dispatch_async block.
I hope this helps you too.
I emailed an Apple engineer last week about a problem with my NSMetadataQuery.
Here’s the email:
Hi,
I'm writing a document-based app or iOS and my method for renaming (moving the document to a new location) seems to conflict with the running NSMetadataQuery.
The query updates a couple of time after the move method is called, the first time it has the old URL of the item that just moved, and the next it has the new URL. However, because of my updating method (below) if a URL has been removed since the update, my model removes the deleted URL and vice versa for if it finds a URL which doesn't exist yet.
I think my problem is one of two issue, either the NSMetadataQuery's update method is insufficient and doesn't check an item's URL for the 'correct' attributes before deleting it (although looking over documentation I can't see anything that would suggest I'm missing something) or my renaming method isn't doing something it should.
I have tried disabling updates at the start of the renaming method and reenabling once all completion blocks are finished but it doesn't make any difference.
My NSMetadataQuery's update method:
func metadataQueryDidUpdate(notification: NSNotification) {
ubiquitousItemsQuery?.disableUpdates()
var ubiquitousItemURLs = [NSURL]()
if ubiquitousItemsQuery != nil && UbiquityManager.sharedInstance.ubiquityIsAvailable {
for var i = 0; i < ubiquitousItemsQuery?.resultCount; i++ {
if let result = ubiquitousItemsQuery?.resultAtIndex(i) as? NSMetadataItem {
if let itemURLValue = result.valueForAttribute(NSMetadataItemURLKey) as? NSURL {
ubiquitousItemURLs.append(itemURLValue)
}
}
}
// Remove deleted items
//
for (index, fileRepresentation) in enumerate(fileRepresentations) {
if fileRepresentation.fileURL != nil && !contains(ubiquitousItemURLs, fileRepresentation.fileURL!) {
removeFileRepresentations([fileRepresentation], fromDisk: false)
}
}
// Load documents
//
for (index, fileURL) in enumerate(ubiquitousItemURLs) {
loadDocumentAtFileURL(fileURL, completionHandler: nil)
}
ubiquitousItemsQuery?.enableUpdates()
}
}
And my renaming method:
func renameFileRepresentation(fileRepresentation: FileRepresentation, toNewNameWithoutExtension newName: String) {
if fileRepresentation.name == newName || fileRepresentation.fileURL == nil || newName.isEmpty {
return
}
let newNameWithExtension = newName.stringByAppendingPathExtension(NotableDocumentExtension)!
// Update file representation
//
fileRepresentation.nameWithExtension = newNameWithExtension
if let indexPath = self.indexPathForFileRepresentation(fileRepresentation) {
self.reloadFileRepresentationsAtIndexPaths([indexPath])
}
UbiquityManager.automaticDocumentsDirectoryURLWithCompletionHandler { (documentsDirectoryURL) -> Void in
let sourceURL = fileRepresentation.fileURL!
let destinationURL = documentsDirectoryURL.URLByAppendingPathComponent(newNameWithExtension)
// Update file representation
//
fileRepresentation.fileURL = destinationURL
if let indexPath = self.indexPathForFileRepresentation(fileRepresentation) {
self.reloadFileRepresentationsAtIndexPaths([indexPath])
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
let coordinator = NSFileCoordinator(filePresenter: nil)
var coordinatorError: NSError?
coordinator.coordinateWritingItemAtURL(sourceURL, options: .ForMoving, writingItemAtURL: destinationURL, options: .ForReplacing, error: &coordinatorError, byAccessor: { (newSourceURL, newDestinationURL) -> Void in
var moveError: NSError?
let moveSuccess = NSFileManager().moveItemAtURL(newSourceURL, toURL: newDestinationURL, error: &moveError)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
assert(moveError == nil || moveSuccess, "Error renaming (moving) document from \(newSourceURL) to \(newDestinationURL).\nSuccess? \(moveSuccess).\nError message: \(moveError).")
if let query = self.ubiquitousItemsQuery {
query.enableUpdates()
}
if moveError != nil || moveSuccess {
// TODO: Implement resetting file rep
}
})
})
})
}
}
I had a reply almost instantly but since then there’s been no reply.
Here’s the reply
One of the big things that jumps out at me is your usage of disableUpdates() and enableUpdates(). You’re executing them both on the same turn of the run loop, but NSMetadataQuery delivers results asynchronously. Since this code executes within your update notification, it is executing synchronously with respect to the query. So from the query’s point-of-view, it’s going to begin delivering updates by posting the notification. Posting a notification is a synchronous process, so while it’s posting the notification, updates will be disabled and the re-enabled. Thus, by the time the query is done posting the notification, it’s back in the exact same state it was in when it started delivering results. It sounds like that’s not the behavior you’re wanting.
Here’s where I need help
I took this to assume that NSMetadataQuery has some kind of cache which it adds results to while updates are disabled and when enabled, those (perhaps many) cache results are looped through and each are sent via the updates notification.
Anyway, I had a look at run loops on iOS and although I understand them as much as I can on my own, I don’t understand how the reply is helpful, i.e how to actually fix the problem - or what’s even causing the problem.
If anyone has any good idea I’d love your help!
Thanks.
Update
Here’s my log of when functions start and end:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start renameFileRepresentation:toNewNameWithoutExtension
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
end renameFileRepresentation:toNewNameWithoutExtension
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
I was having the same problem. NSMetaDataQuery updates tell you if there is a change, but does not tell you what that change was. If the change is a rename, there is no way to identify the previous name, so I can find the old entry in my tableView. Very frustrating.
But, you can get the information by using NSFileCoordinator and NSFilePresenter.
Use the NSFilePresenter method presentedSubitemAtURL(oldURL: NSURL, didMoveToURL newURL: NSURL)
As you noted, the query changed notification is called once with the old URL, and once with the new URL. The method above is called between those two notifications.