Swift 2 iOS - get file list sorted by creation date - more concise solution?

12,443

Solution 1

A possible solution:

if let urlArray = try? NSFileManager.defaultManager().contentsOfDirectoryAtURL(directory,
    includingPropertiesForKeys: properties, options:.SkipsHiddenFiles) {

    return urlArray.map { url -> (String, NSTimeInterval) in
        var lastModified : AnyObject?
        _ = try? url.getResourceValue(&lastModified, forKey: NSURLContentModificationDateKey)
        return (url.lastPathComponent!, lastModified?.timeIntervalSinceReferenceDate ?? 0)
    }
    .sort({ $0.1 > $1.1 }) // sort descending modification dates
    .map { $0.0 } // extract file names

} else {
    return nil
}

The array of URLs is mapped to an array of (lastPathComponent, lastModificationDate) tuples first, then sorted according to the last modification date, and finally the path name extracted.

The attributesDictionary can be avoided by using getResourceValue(_ : forKey) to retrieve only the last modification date.

Update for Swift 3:

let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
if let urlArray = try? FileManager.default.contentsOfDirectory(at: directory,
                                                               includingPropertiesForKeys: [.contentModificationDateKey],
                                                               options:.skipsHiddenFiles) {

    return urlArray.map { url in
            (url.lastPathComponent, (try? url.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? Date.distantPast)
        }
        .sorted(by: { $0.1 > $1.1 }) // sort descending modification dates
        .map { $0.0 } // extract file names

} else {
    return nil
}

Solution 2

Swift 5.0 Uses Filter and Sort by ModificationDate

let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
guard let directoryURL = URL(string: paths.path) else {return}
do {
   let contents = try
   FileManager.default.contentsOfDirectory(at: directoryURL, 
          includingPropertiesForKeys:[.contentModificationDateKey], 
          options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
       .filter { $0.lastPathComponent.hasSuffix(".swift") }
       .sorted(by: {
           let date0 = try $0.promisedItemResourceValues(forKeys:[.contentModificationDateKey]).contentModificationDate!
           let date1 = try $1.promisedItemResourceValues(forKeys:[.contentModificationDateKey]).contentModificationDate!
           return date0.compare(date1) == .orderedDescending
        })
  
    // Print results    
    for item in contents {
        guard let t = try? item.promisedItemResourceValues(forKeys:[.contentModificationDateKey]).contentModificationDate 
            else {return}
        print ("\(t)   \(item.lastPathComponent)")
    }
} catch {
    print (error)
}

Solution 3

Swift 5.0. Simple extension based on the previous answers:

extension FileManager {

    enum ContentDate {
        case created, modified, accessed

        var resourceKey: URLResourceKey {
            switch self {
            case .created: return .creationDateKey
            case .modified: return .contentModificationDateKey
            case .accessed: return .contentAccessDateKey
            }
        }
    }

    func contentsOfDirectory(atURL url: URL, sortedBy: ContentDate, ascending: Bool = true, options: FileManager.DirectoryEnumerationOptions = [.skipsHiddenFiles]) throws -> [String]? {

        let key = sortedBy.resourceKey

        var files = try contentsOfDirectory(at: url, includingPropertiesForKeys: [key], options: options)

        try files.sort {

            let values1 = try $0.resourceValues(forKeys: [key])
            let values2 = try $1.resourceValues(forKeys: [key])

            if let date1 = values1.allValues.first?.value as? Date, let date2 = values2.allValues.first?.value as? Date {

                return date1.compare(date2) == (ascending ? .orderedAscending : .orderedDescending)
            }
            return true
        }
        return files.map { $0.lastPathComponent }
    }
}

Solution 4

Swift 3 Code With Complete Solution: Based on @ingconti's answer. This method return list of item names from provided URL path.

func filesSortedList(atPath: URL) -> [String]? {

    var fileNames = [String]()
    let keys = [URLResourceKey.contentModificationDateKey]

    guard let fullPaths = try? FileManager.default.contentsOfDirectory(at: atPath, includingPropertiesForKeys:keys, options: FileManager.DirectoryEnumerationOptions.skipsHiddenFiles) else {
        return [""]
    }

    let orderedFullPaths = fullPaths.sorted(by: { (url1: URL, url2: URL) -> Bool in
        do {
            let values1 = try url1.resourceValues(forKeys: [.creationDateKey, .contentModificationDateKey])
            let values2 = try url2.resourceValues(forKeys: [.creationDateKey, .contentModificationDateKey])

            if let date1 = values1.creationDate, let date2 = values2.creationDate {
                //if let date1 = values1.contentModificationDate, let date2 = values2.contentModificationDate {
                return date1.compare(date2) == ComparisonResult.orderedDescending
            }
        } catch _{

        }
        return true
    })

    for fileName in orderedFullPaths {
        do {
            let values = try fileName.resourceValues(forKeys: [.creationDateKey, .contentModificationDateKey])
            if let date = values.creationDate{
                //let date : Date? = values.contentModificationDate
                print(fileName.lastPathComponent, " ", date)
                let theFileName = fileName.lastPathComponent
                fileNames.append(theFileName)
            }
        }
        catch _{

        }
    }
    return fileNames
}

Solution 5

return urlDictionary.filter{$0 != nil}.sort{$0.1.compare($1.1) == NSComparisonResult.OrderedDescending }.map{$0.0}.map{$0.lastPathComponent!}

is definitely an overkilling line of code :) You can skip a couple of filter/map steps by using another method of NSFileManager:

func contentsOfDirectoryAtPath(_ path: String) throws -> [String] 

and

func attributesOfItemAtPath(_ path: String) throws -> [String : AnyObject].

In the end, you would end up with something equivalent to what you have already done. I think your code is a little bit sophisticated, but the approach is quite good.

Share:
12,443

Related videos on Youtube

simons
Author by

simons

(My old SO answers from the early Swift era are now very out of date) I am a web developer - currently most often using: C# / .Net Core / Bootstrap 5 / SCSS / SQL Server / JavaScript / AWS / Azure / Elasticsearch / Google Maps API Git & Jira. Previously - Swift / Wordpress / PHP / MySQL / C++ I am also proficient in setting up and maintaining server and app service environments - both Linux and Windows. Background: MSc Information Systems

Updated on October 22, 2022

Comments

  • simons
    simons over 1 year

    In my code, which works, I am returning a [String]? containing the names of the files (lastPathComponent) stored in /Documents/ - ordered by the date last modified.

    I believe that I am probably using too many steps and am looking for advice here re how to reduce the code.

    In order to achieve the required result currently - I am creating two intermediate dictionaries: var attributesDictionary: [String : AnyObject]? and var urlDictionary = [NSURL:NSDate](). Looping through the initial [NSURL] I am using two steps - .resourceValuesForKeys initializes attributesDictionary. I then populate urlDictionary so that it contains the URL and the value for the key NSURLContentModificationDateKey.

    I feel fairly certain that there should be a way to achieve this result without creating urlDictionary and attributesDictionary and without the need for the loop. Perhaps from urlArray directly. Here is my current code:

    EDIT: do{}s were not required as pointed out by Arthur Gevorkyan in the first comment.

    func getFileList() -> [String]? {
        let directory = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
        let properties = [NSURLLocalizedNameKey, NSURLCreationDateKey, NSURLContentModificationDateKey, NSURLLocalizedTypeDescriptionKey]
    
        // no catch required - contentsOfDirectoryAtURL returns nil if there is an error
        if let urlArray = try? NSFileManager.defaultManager().contentsOfDirectoryAtURL(directory, includingPropertiesForKeys: properties, options:NSDirectoryEnumerationOptions.SkipsHiddenFiles) {
            var attributesDictionary: [String:AnyObject]?
            var dateLastModified: NSDate
            var urlDictionary = [NSURL:NSDate]()
    
            for URLs in urlArray {
                // no catch required - resourceValuesForKeys returns nil if there is an error
                attributesDictionary = try? URLs.resourceValuesForKeys(properties)
                dateLastModified = attributesDictionary?[NSURLContentModificationDateKey] as! NSDate
                urlDictionary[URLs] = dateLastModified
            }
            // this approach to sort is used because NSDate cannot be directly compared with </>
            return urlDictionary.filter{$0 != nil}.sort{$0.1.compare($1.1) == NSComparisonResult.OrderedDescending }.map{$0.0}.map{$0.lastPathComponent!}
        } else {
            return nil
        }
    }
    
    • Arthur Gevorkyan
      Arthur Gevorkyan over 8 years
      "do { // no catch required - contentsOfDirectoryAtURL returns nil if there is an error" - sounds right as you convert errors to optionals within the do scope. Moreover, you don't need the do scope either.
    • simons
      simons over 8 years
      Thanks for that. Yes - neither of the do{}s were required.
  • Abizern
    Abizern over 8 years
    You can filter out Optionals with .flatMap{ $0 }
  • Arthur Gevorkyan
    Arthur Gevorkyan over 8 years
    Thanks. I will definitely use this when the time comes :)
  • simons
    simons over 8 years
    Thanks. That's a great answer.
  • simons
    simons over 8 years
    Thanks for this. For now I have gone with the solution from Martin R - but I will be taking the time to look into your suggestion further.
  • Arthur Gevorkyan
    Arthur Gevorkyan over 8 years
    No problem. Martin's answer is ready to be used (leave alone good) whereas mine is just a plan. Good luck :)
  • Crashalot
    Crashalot over 8 years
    Hi @Martin what's the best way to reach you? Have a quick question to ask you. Thanks! BTW your answers have been hugely helpful on SO. Thanks so much for contributing your time.
  • mriaz0011
    mriaz0011 about 7 years
    Thanks @EricAya for giving me suggestions. I edited my answer. You can uncomment the code to get items list for last modified date. You can get items list either for created date or last modified date. Hope It will help you to use this method.
  • VyacheslavBakinkskiy
    VyacheslavBakinkskiy over 3 years
    @Martin R in your example 'urlArray' have type [String], how can I replace it to [URL]?
  • Martin R
    Martin R over 3 years
    @VyacheslavBakinkskiy: Just replace url.lastPathComponent by url !
  • ChuckZHB
    ChuckZHB almost 3 years
    @Martin R, thanks. I need to add a feature to show the recently added songs on a VC, just thinking how to sort the music files per date in sandbox and I found this. Never thought that URL array could include date key, because it is just an URL array on the paper.