iCloud NSMetadataQuery and updates (NSMetadataQueryUpdateChangedItemsKey) - ios

I am monitoring my iCloud sandbox (iOS) using an NSMetaDataQuery are recommended - and all is working well.
I'm attempting to use the NSMetadataQueryUpdateChangedItemsKey in the NSMetadataQueryDidUpdateNotification in order to efficiently update my internal model of the file system. Challenge I have is that when a file is moved/renamed, how can I know the original file path - so I can update my model?
It appears that the NSMetaDataItem objects are persistent (i.e. the same object instance is updated when the path changes) so I could use the pointer value as a kind of index into my model. However - I'd be taking advantage of an apparent implementation detail (which could change.) Perhaps NSMetaDataItems are recycled when memory runs low?
Anyone know how this should be done (or if it is actually the case that NSMetaDataItem objects persist for the lifetime of the NSMetaDataQuery - and stay 'attached' to the same file system item.)

Yes, the NSMetadataQuery doesn't provide a way to consult the previous path.
When an item is moved, its index in the NSMetadataQuery results remains the same. So we can duplicate the path of the results and when the update kicks in, we only need to check the NSMetadataItem at the exact position of the duplicated array.
if let updatedObj = obj.userInfo?[NSMetadataQueryUpdateChangedItemsKey] as! [NSMetadataItem]? {
for it in updatedObj {
let url = it.valueForAttribute(NSMetadataItemURLKey) as! NSURL
let value = it.valueForAttribute(NSMetadataUbiquitousItemIsUploadedKey) as! NSNumber
print("Path: " + url.path!)
print("Updated: " + value.stringValue)
let index = metaDataQuery.indexOfResult(it)
let prevPath = duplicatedPathArray[index]
if (prevPath != url.path!) {
print("File Moved. Previous path: " + prevPath)
duplicatePath()
}
}
}
Make sure you update the array each time a file is added or removed.

Documentation mentions that results are suitable for Cocoa Bindings, which means that most likely those objects are persistent.
I use more hardcore combination of NSFilePresenter and NSMetadataQuery running side by side to monitor documents in container. NSFilePresenter has convenient API for detecting when files were being moved:
func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL)
For that to work though when you move files in container you have to explicitly notify file coordinator that you're moving file (see points 1-3):
let fc = NSFileCoordinator()
var error: NSError?
fc.coordinate(writingItemAt: from, options: .forMoving, writingItemAt: to, options: .forReplacing, error: &error, byAccessor: {
(fromURL, toURL) in
do {
// 1
fc.item(at: fromURL, willMoveTo: toURL)
try FileManager.default.moveItem(at: fromURL, to: toURL)
// 2
fc.item(at: fromURL, didMoveTo: toURL)
} catch {
// 3
fc.item(at: fromURL, didMoveTo: fromURL)
}
})

Related

FileProvider: "CopyItem()" is called twice -> error (FTP download)

The first view of my app (Swift 5, Xcode 10, iOS 12) has a "username" TextField and a "login" Button. Clicking on the button checks if there's a file for the entered username on my FTP server and downloads it to the Documents folder on the device. For this I'm using FileProvider.
My code:
private func download() {
print("start download") //Only called once!
let foldername = "myfolder"
let filename = "mytestfile.txf"
let server = "192.0.0.1"
let username = "testuser"
let password = "testpw"
let credential = URLCredential(user: username, password: password, persistence: .permanent)
let ftpProvider = FTPFileProvider(baseURL: server, mode: FTPFileProvider.Mode.passive, credential: credential, cache: URLCache())
ftpProvider?.delegate = self as FileProviderDelegate
let fileManager = FileManager.default
let source = "/\(foldername)/\(filename)"
let dest = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(filename)
let destPath = dest.path
if fileManager.fileExists(atPath: destPath) {
print("file already exists!")
do {
try fileManager.removeItem(atPath: destPath)
} catch {
print("error removing!") //TODO: Error
}
print("still exists: \(fileManager.fileExists(atPath: destPath))")
} else {
print("file doesn't already exist!")
}
let progress = ftpProvider?.copyItem(path: source, toLocalURL: dest, completionHandler: nil)
progressBar.observedProgress = progress
}
I'm checking if the file already exists on the device because FileProvider doesn't seem to provide a copyItem function for downloading that also lets you overwrite the local file.
The problem is that copyItem tries to do everything twice: Downloading the file the first time succeeds (and it actually exists in Documents, I checked) because I manually delete the file if it already exists. The second try fails because the file already exists and this copyItem function doesn't know how to overwrite and of course doesn't call my code to delete the original again.
What can I do to fix this?
Edit/Update:
I created a simple "sample.txt" at the root of my ftp server (text inside :"Hello world from sample.txt!"), then tried to just read the file to later save it myself. For this I'm using this code from the "Sample-iOS.swift" file here.
ftpProvider?.contents(path: source, completionHandler: {
contents, error in
if let contents = contents {
print(String(data: contents, encoding: .utf8))
}
})
But it also does this twice! The output for the "sample.txt" file is:
Optional("Hello world from sample.txt!")
Fetching on sample.txt succeed.
Optional("Hello world from sample.txt!Hello world from sample.txt!")
Fetching on sample.txt succeed.
Why is it calling this twice too? I'm only calling my function once and "start download" is also only printed once.
Edit/Update 2:
I did some more investigating and found out what's called twice in the contents function:
It's the whole self.ftpDownload section!
And inside FTPHelper.ftpLogin the whole self.ftpRetrieve section is
called twice.
And inside FTPHelper.ftpRetrieve the whole self.attributesOfItem
section is called twice.
And probably so on...
ftpProvider?.copyItem uses the same ftpDownload func, so at least I know why both contents() and copyItem() are affected.
The same question remains though: Why is it calling these functions twice and how do I fix this?
This isn't an answer that shows an actual fix for FileProvider!
Unfortunately the library is pretty buggy currently, with functions being called twice (which you can kind of prevent by using a "firstTimeCalled" bool check) and if the server's slow(-ish), you also might not get e.g. the full list of files in a directory because FileProvider stops receiving answers before the server's actually done.
I haven't found any other FTP libraries for Swift that work (and are still supported), so now I'm using BlueSocket (which is able to open sockets, send commands to the server and receive commands from it) and built my own small library that can send/receive,... files (using the FTP codes) around it.

SCNParticleSystem load from document Directory

I am trying load SCNParticleSystem from download bundle which i am not able to load.
Path for the resource.
file:///var/mobile/Containers/Data/Application/A91E9970-CDE1-43D8-B822-4B61EFC6149B/Documents/so/solarsystem.bundle/Contents/Resources/
let objScene = SCNParticleSystem(named: "stars", inDirectory: directory)
This object is nil.
This is a legitimate problem since SceneKit does not provide an out-of-the-box solution for initializing particle systems from files that are outside of the main bundle (the only init method SCNParticleSystem.init(named:inDirectory:) implies that SCNParticleSystem.scnp files are in the main bundle).
Luckily for us .scnp files are just encoded/archived SCNParticleSystem instances that we can easily decode/unarchive using NSKeyedUnarchiver:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
return system
}
}
If you do not need to support iOS 9 and iOS 10 you can use NSKeyedUnarchiver.unarchivedObject(ofClass: SCNParticleSystem.self, from: data) instead of NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(_:) and type casting, which was introduced in iOS 11.0.
Another issue that you're most likely to encounter is missing particle images. That is because by default SceneKit will look for them in the main bundle. As of current versions of iOS (which is iOS 12) and Xcode (Xcode 10) particle images in .scnp files (particleImage property) are String values which are texture filenames in the main bundle (that might change, but probably won't, however there's not much else we could use).
So my suggestion is to take that filename and look for the texture file with the same name in the same directory where the .scnp file is:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
system.particleImage = particleImageURL
}
}
return system
}
}
You can just set the URL of the image file and SceneKit will handle it from there.
As a little side-note, the recommended directory for downloadable content is Application Support directory, not Documents.
Application Support: Use this directory to store all app data files except those associated with the user’s documents. For example, you might use this directory to store app-created data files, configuration files, templates, or other fixed or modifiable resources that are managed by the app. An app might use this directory to store a modifiable copy of resources contained initially in the app’s bundle. A game might use this directory to store new levels purchased by the user and downloaded from a server.
(from File System Basics)
Don't have enough reps to add the comment so adding it as the answer.
The answer by Lësha Turkowski works for sure but was had issues with loading the particle images using only NSURL.
All particles were appearing square which meant,
If the value is nil (the default), SceneKit renders each particle as a
small white square (colorized by the particleColor property).
SCNParticleSystem particleImage
In the documentation it says You may specify an image using an
NSImage (in macOS) or UIImage (in iOS) instance, or an NSString or
NSURL instance containing the path or URL to an image file.
Instead of using the NSURL, ended up using the UIImage and it loaded up fine.
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
// load up the NSURL contents in UIImage
let particleUIImage = UIImage(contentsOfFile: particleImageURL.path)
system.particleImage = particleUIImage
}
}
return system
}
}
I found out, that sometimes when dragging a SCNParticleSystem file into your project (probably form a different project) a silent error can happen due to some bugs in Xcode. As a result you can't get a reference to an instance of your SCNParticleSystem.
Solution: Check your BuildSettings in your target. The SCNPaticleSystem AND the associated ImageFile should be listed there and then you should get it right. (see screenShot below)

How do I know when an NSManagedObject will be deleted from the persistent store?

So, for the purposes of saving space and caching, I defined a Photo model in CoreData that has an attribute imageDataURL (a fileURL).
This data would be stored in the Documents directory. As such, I want to make sure I am cleaning up that data if the user deletes the Photo object.
My question is, where should I look to have a deleteDataAtImageURL(...) method?
I'm thinking it would be in the prepareForDeletion() method on NSManagedObject and I check if that object's context's parent is nil. This tells me that it is a context directly in contact with the Persistent Store.
This should work, unless of course the user resets the context and doesn't save it.
I can't imagine I'm the first one to want to do this, so any advice on this approach (or a better one!) would be appreciated!
I had a similar issue. Here's how I solved it by using a NotificationCenter observer for an action on my root saving context.
//done as part of a singleton class setup
NotificationCenter.default.addObserver(self, selector: #selector(SingletonClass.handleModelDataChange), name:NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: yourRootSavingContext)
internal func handleModelDataChange(notification: NSNotification) {
//get documents directory
let documentsURL = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
//get deleted items from dictionary
if let deleted = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> , deleted.count > 0 {
for object in deleted{
//sort out your objects of interest, I cared about objects of a certain class
if ...{
do {
//delete filepath
try FileManager.default.removeItem(at: documentsURL.appendingPathComponent(myPathComponent, isDirectory: true))
//print("Deleted the folder \(documentsURL.URLByAppendingPathComponent(myPathComponent, isDirectory: true))")
}
catch {
//print("I tried :(")
print(error)
}
}
}
}
}
In the end the solution for me was to override prepareForDeletion and check if the the object's .managedObjectContext.parent property is nil. That tells me that it's connected to the PersistentStore, and there I can do the task I want to.
I don't know if this is best practices, but it is working.
I believe the above answer from sschale would also work although I didn't try it.

How to cache images in IOS App with expiry age using swift

In an iOS app, how can I cache an image with specified expiry age? There are examples on how to store and retrieve images, but how can I set an expiry period to auto delete old images?
As indicated by Fahri, you will need to manage the cache yourself (or using an open source library). You could easily create a cache directory to store your images. Then, at application launch, you parse this image cache directory to check image creation date, check time elapsed and remove those older than the specified age.
The below Swift code will do this parsing/removing job, I set the specified age to 30,000 (seconds)
// We list the stored images in Caches/Images and delete old ones
let cacheDirectory = NSFileManager.defaultManager().URLsForDirectory(.CachesDirectory, inDomains: .UserDomainMask).first! as NSURL
let filelist = try? filemanager.contentsOfDirectoryAtPath(cacheDirectory.path!)
var newDir = cacheDirectory.URLByAppendingPathComponent("Images")
var properties = [NSURLLocalizedNameKey, NSURLCreationDateKey, NSURLLocalizedTypeDescriptionKey]
var URLlist = try? filemanager.contentsOfDirectoryAtURL(newDir, includingPropertiesForKeys: properties, options: [])
if URLlist != nil {
for URLname in URLlist! {
let filePath = URLname.path!
let attrFile: NSDictionary? = try? filemanager.attributesOfItemAtPath(filePath)
let createdAt = attrFile![NSFileCreationDate] as! NSDate
let createdSince = fabs( createdAt.timeIntervalSinceNow )
#if DEBUG
print( "file created at \(createdAt), \(createdSince) seconds ago" )
#endif
if createdSince > 30000 {
let resultDelete: Bool
do {
try filemanager.removeItemAtPath(filePath)
resultDelete = true
} catch _ {
resultDelete = false
}
#if DEBUG
print("purging file =\(filePath), result= \(resultDelete)")
#endif
}
}
}
Web is web, iOS is iOS. If you want to create image cache with expiration, you have to implement it yourself, or use open source lib. I can give you the idea, it's not hard to implement. So, in addition to storing and retrieving functionality, you also need to add metadata management methods, using which you could know when the image was added, and what's the expiration date for that image, and when some events occur (app become active, going to background etc.) you should check the meta for your images, and delete the image if the expiration date passed. That's it, nothing hard. Good luck!
P.S.: In some git source projects I have seen the functionality your are looking for, check DFCache on github, maybe it suits your needs.

Will images be removed from within the local filesystem on update in iOS?

I am saving images captured from the camera within the local sandbox (filesystem) and store the filepath within my app to show the images (using Swift). I see that if I hit play in XCode, the images will be removed (which is ok)
Now I wonder what would happen if I submit this to the app store, the user saves images and I will update the app later on. Will the images will be removed as well?
To store the image, I use this function..
func saveImageLocally(imageData:NSData!) -> String{
let time = NSDate().timeIntervalSince1970
let fileManager = NSFileManager.defaultManager()
let dir = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0].stringByAppendingPathComponent(subDirForImage) as String
if !fileManager.fileExistsAtPath(dir) {
var error: NSError?
if !fileManager.createDirectoryAtPath(dir, withIntermediateDirectories: true, attributes: nil, error: &error) {
println("Unable to create directory: \(error)")
return ""
}
}
let path = dir.stringByAppendingPathComponent("IMAGENAME\(Int(time)).png")
var error: NSError?
if !imageData.writeToFile(path, options: NSDataWritingOptions.DataWritingAtomic, error: &error) {
println("error writing file: \(error)")
return ""
}
return path
}
Anything you store in your documents folder will persist through app updates providing you keep the same app ID and increment the version number.
Though it is worth noting that the full path to the app sandbox will be different (there will be a new sandbox for the update and the old data copied into it), so make sure you are only accessing resources by storing relative paths etc and are not storing full paths to images and expecting them to resolve after an update.

Resources