How to sort the filemanager array by creation date - swift

I have written code that retrieves CSV paths in the documents directory and loads them into a tableview. I am trying to sort the files by creation date so that they list from newest to oldest in the tableview. Anyone got any advice on how to accomplish this?
I've not tried anything yet, because I am a little stuck
override func viewDidLoad() {
super.viewDidLoad()
csvFiles = listCsvs()
tblViewDataCSV.dataSource = self
tblViewDataCSV.delegate = self
}
func listCsvs() -> [URL] {
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let files = try? fileManager.contentsOfDirectory(
at: documentDirectory,
includingPropertiesForKeys: nil,
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles]
).filter {
$0.lastPathComponent.hasSuffix(".csv")
}
print(files as Any)
return files ?? []
}
I need the array sorted by .creationdate and not alphanumerically. Many thanks for your help.

You need to declare a struct that has a URL (or filename String) and a Date. Populate an array of this struct from the files (and their creation dates) you query from FileManager.
Use that array of struct as your data model for the table view. You can sort the array on filename or date (or any other attributes you might add in the future).
You can get the creation date of each file by first adding [.creationDateKey] to the includingPropertiesForKeys parameter of contentsOfDirectory. Then access the creation date using resourceValues on each URL. See How can I get the file creation date using URL resourceValues method in Swift 3? for more details on getting the creation date.
It may help to use the enumerator method of FileManager instead of contentsOfDirectory. This will make it easier to get the needs URL attributes and populate the array of struct.

You can build a struct like the below:
struct yourStruct {
var path:URL
var filedate:Date
}
For each file in folder, append your struct into array.
let s = yourStruct(
path: csvUrl,
filedate: csvFileDate)
myArray.append(s)
At this point, you have an array with your files and filedates.
And finally sort the array with:
let newArr = myArray.sorted { $0.filedate < $1.filedate }

Related

How can I iterate inside each directory in resource path

I have a simple scenario, there is a folder called content that contains image file, I want all the image file starting with nssl to be saved to an array , so I do below code, but I cannot seem to think or know a way to find out how I can move in each directory and search for such a file and append to my array , here is my code below , I can get the names of all the directories, but what to do next ?
let path = Bundle.main.resourcePath!
let fm = FileManager.default
do {
let items = try fm.contentsOfDirectory(atPath: path)
for item in items {
}
} catch {
}
FileManager is not needed.
Bundle provides urls(forResourcesWithExtension: subdirectory:) which returns multiple urls for a specific extension
if let urls = Bundle.main.urls(forResourcesWithExtension: "png", subdirectory: "content") {
for url in urls where url.lastPathComponent.hasPrefix("nssl") {
}
}
Change the png extension to the desired type.

MacOS how is KIND implemented

I am trying to write a piece of code that instead of checking file extensions for the many different types of image files, but instead looking at the file attributes. What I can’t figure out from searching the docs is if KIND:Image is really a file attribute or simply a construct Apple created in the FinderApp to make things easier for the user.
I wrote a snippet that pulls the attributes for files with an extension of jpeg and for each file the fileType is returned as NSFileTypeRegular.
let attr = try filemanager.attributesOfItem(atPath: pathConfig+"/"+file) as NSDictionary
if file.hasSuffix(ext) {
   print ("Adding \(file) [ \(attr.fileSize()) \(attr.fileType())]")
   print ( attr.fileModificationDate() )
}
Does anybody know if MacOS retains an attribute for the category a file falls in to. e.g. IMAGE, DOCUMENT etc.
To achieve a functionality similar to the Kind search tag in Finder you can use UTType (Link to reference).
You can get the UTType of a file by initialising it with the file extension:
let fileType = UTType(filenameExtension: fileURL.pathExtension)
The cool thing about UTTypes is that they have a hierarchy, for example, UTType.jpeg is a subtype of UTType.image, along with others like .png.
If you want to check if a file is any kind of image, you can do it like this
let isImage = fileType.conforms(to: .image)
You can check the list for the kind of types you want to support as "Kinds", and filter using those UTTypes
This was my final solution based on the information provided by #EmilioPelaez I am not completely comfortable with Swift especially the unwrapping operations so if the code looks weird that might be why.
func imagesInDir(path: String?) -> [String] {
if let path {
let filemanager: FileManager = FileManager()
let files = filemanager.enumerator(atPath: path)
var array = [String]()
var urlFile: NSURL
while let file = files?.nextObject() as? String {
urlFile = NSURL(fileURLWithPath: file, isDirectory: false)
if (urlFile.isFileURL) {
if let pathExt = urlFile.pathExtension {
if let fileType = UTType(filenameExtension: pathExt) {
let isImage = fileType.conforms(to: .image)
if (isImage){
array.append(file)
print ("\(array.count) \(fileType)")
}
}
}
}
}
}
return array
}

Realm Swift Bundle Data

I converted a csv file to a realm file and I want to use it in my app.
This is my code atm:
func inLibrarayFolder(fileName: String) -> URL {
return URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)[0], isDirectory: true)
.appendingPathComponent(fileName)
}
func copyPreBundleDataCompeletely() {
let mainRealmUrl = inLibrarayFolder(fileName: "main.realm")
let bundleUrl = Bundle.main.url(forResource: "treesFull", withExtension: "realm")!
//After launch after fresh install (if main.realm never created)
if (!FileManager.default.fileExists(atPath: mainRealmUrl.path)){
//copy bundled data into writable location compeletely
try! FileManager.default.copyItem(
at: bundleUrl, to: mainRealmUrl)
print(mainRealmUrl)
}
}
During the first launch, it creates the new file, but the file is a bit different from the original:
original db
copied db
the Tree object:
class Tree: Object {
#objc dynamic var id: Int32 = 0
#objc dynamic var br = ""
#objc dynamic var nm1 = ""
#objc dynamic var nm2 = ""
#objc dynamic var nm3 = ""
#objc dynamic var longitude = 0.0
#objc dynamic var latitude = 0.0
// override static func primaryKey() -> String? {
// return "id"
// }
}
It looks like I have 2 databeses in the new file, how can I access the second one with the data or how can I copy the file properly?
Also, whats gonna happen when I make the id to a primary key? Obviously I dont have a parameter like that in the original downloaded file, so I guess I will need to migrate the data somehow...
When it comes to importing, the file being imported has to be in a very specific format along with a specific file name
Your Realm object name is Tree, so the imported file name needs to match
Tree.csv
along with that the first line of the file needs to match the classes property names, comma separated
id,br,nm1...
I would suggest creating a very small test file to import with 3-4 lines to get it working. Then, once you mastered that then import the big file.

iOS Swift - reading a persisted data model after having altered properties in that model

Thanks in advance for your help.
I want to persist data such as a user's stats. Let's say I have a data model, a class 'Stats' with a few properties, and it gets saved to the user's device. Supposing that I've released the app, users are recording their stats but then later on I want to make changes to the class - more or fewer properties, maybe even renaming them (etc.), ahead of a new build release. But after these changes have been made, the type 'Stats' is now different to the one the users have saved on their device, so it won't be able to decode and it seems like all the user's previous data up until that point would be lost/unattainable.
How can I add make these kinds of changes to the class in a way in which the PropertyListDecoder will still be able to decode the stats that are still on the user's device?
This is basically what I have:
class Stat: Codable {
let questionCategory = questionCategory()
var timesAnsweredCorrectly: Int = 0
var timesAnsweredFirstTime: Int = 0
var timesFailed: Int = 0
static func saveToFile(stats: [Stat]) {
let propertyListEncoder = PropertyListEncoder()
let encodedSettings = try? propertyListEncoder.encode(stats)
try? encodedSettings?.write(to: archiveURL, options: .noFileProtection)
}
static func loadFromFile() -> [Stat]? {
let propertyListDecoder = PropertyListDecoder()
if let retrievedSettingsData = try? Data(contentsOf: archiveURL), let decodedSettings = try? propertyListDecoder.decode([Stat].self, from: retrievedSettingsData) {
return decodedSettings
} else {
return nil
}
}
}
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let archiveURL = documentsDirectory.appendingPathComponent("savedVerbStats").appendingPathExtension("plist")
It seems that even just adding a new property to 'Stat' will cause the user's previous persisted data to become un-decodable as type 'Stat', and loadFromFile() will return nil.
Any advice would be great! I'm sure I'm going about this the wrong way. I figured that the array [Stat] would be too big to persist in UserDefaults but even then I think this problem would still exist... Can't find anything about it online; it seems that once you've got your users using a persisted class you can't then alter it. I tried using default values for the new properties but the result is the same.
The only solution I can think of is breaking down the class into literals and saving all these in some kind of a tuple/dictionary form instead. Then I would decode that raw data, and have a function to assemble and create the class out of whatever relevant data can still be taken from the old version of the 'Stat' type. Seems like a big workaround and I'm sure you guys know a much a better way.
Thanks!!
Removing a property is easy enough. Just delete its definition from the Stat class and existing data for that property will be deleted when you read and save stats again.
The key to adding new properties is to make them optional. For example:
var newProperty: Int?
When a previously existing stat is decoded the first time, this property will be nil, but all the other properties will be set correctly. You can set and save the new property as needed.
It may be a minor inconvenience to have all new properties as optional, but it opens the door to other possible migration schemes without losing data.
EDIT: Here is a more complicated migration scheme that avoids optionals for new properties.
class Stat: Codable {
var timesAnsweredCorrectly: Int = 0
var timesAnsweredFirstTime: Int = 0
var timesFailed: Int = 0
//save all stats in the new Stat2 format
static func saveToFile(stats: [Stat2]) {
let propertyListEncoder = PropertyListEncoder()
let encodedSettings = try? propertyListEncoder.encode(stats)
try? encodedSettings?.write(to: archiveURL, options: .noFileProtection)
}
//return all stats in the new Stat2 format
static func loadFromFile() -> [Stat2]? {
let propertyListDecoder = PropertyListDecoder()
//first, try to decode existing stats as Stat2
if let retrievedSettingsData = try? Data(contentsOf: archiveURL), let decodedSettings = try? propertyListDecoder.decode([Stat2].self, from: retrievedSettingsData) {
return decodedSettings
} else if let retrievedSettingsData = try? Data(contentsOf: archiveURL), let decodedSettings = try? propertyListDecoder.decode([Stat].self, from: retrievedSettingsData) {
//since we couldn't decode as Stat2, we decoded as Stat
//convert existing Stat instances to Stat2, giving the newProperty an initial value
var newStats = [Stat2]()
for stat in decodedSettings {
let newStat = Stat2()
newStat.timesAnsweredCorrectly = stat.timesAnsweredCorrectly
newStat.timesAnsweredFirstTime = stat.timesAnsweredFirstTime
newStat.timesFailed = stat.timesFailed
newStat.newProperty = 0
newStats.append(newStat)
}
return newStats
} else {
return nil
}
}
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let archiveURL = documentsDirectory.appendingPathComponent("savedVerbStats").appendingPathExtension("plist")
}
class Stat2: Stat {
var newProperty: Int = 0
}
Following on from Mike's response, I came up with a migration scheme that seems to solve the issue of the optionals and doesn't require any new classes every time the data model is altered. Part of the issue is that a developer may change or add properties to a class that gets persisted and Xcode would never flag this as an issue, which may result in your user's app trying to read the previous data class saved to the device, returning nil and in all likelihood overwriting all the data in question with a reformatted model.
Instead of writing the class (e.g. Stat) to the disk (which is what Apple suggests in its teaching resources), I save a new struct "StatData" which comprises only optional properties of the data that I want to write to the file:
struct StatData: Codable {
let key: String
let timesAnsweredCorrectly: Int?
let timesAnsweredFirstTime: Int?
let timesFailed: Int?
}
That way I can read the properties from the file and any added or deleted properties from the struct would just return nil instead of making the entire struct unreadable. Then I have two functions to convert 'StatData' into 'Stat' (and back), providing default values in case any have been returned nil.
static func convertToData(_ stats: [Stat]) -> [StatData] {
var data = [StatData]()
for stat in stats {
let dataItem = StatData(key: stat.key, timesAnsweredCorrectly: stat.timesAnsweredCorrectly, timesAnsweredFirstTime: stat.timesAnsweredFirstTime, timesFailed: stat.timesFailed)
data.append(dataItem)
}
return data
}
static func convertFromData(_ statsData: [StatData]) -> [Stat] {
// if any of these properties weren't previously saved to the device, they will return the default values but the rest of the data will remain accessible.
var stats = [Stat]()
for item in statsData {
let stat = stat.init(key: item.key, timesAnsweredCorrectly: item.timesAnsweredCorrectly ?? 0, timesAnsweredFirstTime: item.timesAnsweredFirstTime ?? 0, timesFailed: item.timesFailed ?? 0)
stats.append(stat)
}
return stats
}
I then call these functions when reading or saving the data to disk. The advantage of this is that I can choose which properties from the Stat class that I want to save, and because the StatData model is a struct, the memberwise initializer will warn any developer who changes the data model that they will also need to account for the change when reading old data from the file.
This seems to do the job. Any comments or other suggestions would be appreciated

NSKeyedArchiver.archiveRootObject fails to save

I have a group of custom objects that I'm converting to NSMutableDictionary's, and then creating an array out of them (this part is currently working as expected).
I'm then attempting to save that array of data as a file using NSKeyedArchiver. However, the result of NSKeyedArchiver.archiveRootObject always returns false.
Below is my logic for saving - am I missing something obvious, or perhaps is this the wrong approach? Thank you!
var groupsArray = [Any?]()
for group in file!.groups{
for obj in group.children {
let objDict = obj.convertToDictionary()
groupsArray.append(objDict)
}
}
let documents: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let filePath: String = URL(fileURLWithPath: documents).appendingPathComponent("file.archive").absoluteString
let save: Bool = NSKeyedArchiver.archiveRootObject(groupsArray, toFile: filePath)
EDIT: This also fails if trying to save to the .desktop or the .caches directories.
The issue here is .absoultestring. If the URL object contains a file URL, then we should use .path for working with FileManager or PathUtilities etc. So here replacing .absoultestring with .path will solve the issue
for more details about their difference please refer this answer
try this method to save
1.Method returns filepath.
func filePath(key:String) -> String {
let manager = FileManager.default
let url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
return (url!.appendingPathComponent(key).path)
}
2.Code to save to a file using NSKeyedArchiver.
NSKeyedArchiver.archiveRootObject(groupsArray, toFile: filePath(key: "file.archive"))