Fetch All Photos from Library based on creationDate in Swift [Faster Way?]

1.7k views Asked by At

I have a UICollectionView displaying library photos based on latest "creationDate". For that I am using below code:

struct AssetsData {
    var creationDate: Date, assetResult: PHFetchResult<PHAsset>
}

func fetchPhotos() -> [AssetsData] {
    //Date Formatter
    let formatter = DateFormatter()
    formatter.dateStyle = DateFormatter.Style.medium
    formatter.timeStyle = DateFormatter.Style.none

    //Photos fetch
    let fetchOptions = PHFetchOptions()
    let sortOrder = [NSSortDescriptor(key: "creationDate", ascending: false)]
    fetchOptions.sortDescriptors = sortOrder
    let assetsFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
    var arrCreationDate = [Date]()
    var arrDates = [String]()

    //Getting All dates
    for index in 0..<assetsFetchResult.count {
        if let creationDate = assetsFetchResult[index].creationDate {
            let formattedDate = formatter.string(from: creationDate)
            if !arrDates.contains(formattedDate) {
                arrDates.append(formattedDate)
                arrCreationDate.append(creationDate)
            }
        }
    }

    //Fetching Assets based on Dates
    var arrPhotoAssetsData = [AssetsData]()
    for createdDate in arrCreationDate {
        if let startDate = getDate(forDay: createdDate.day, forMonth: createdDate.month, forYear: createdDate.year, forHour: 0, forMinute: 0, forSecond: 0), let endDate = getDate(forDay: createdDate.day, forMonth: createdDate.month, forYear: createdDate.year, forHour: 23, forMinute: 59, forSecond: 59) {
            fetchOptions.predicate = NSPredicate(format: "creationDate > %@ AND creationDate < %@", startDate as NSDate, endDate as NSDate)
            let assetsPhotoFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
            arrPhotoAssetsData.append(AssetsData(creationDate: createdDate, assetResult: assetsPhotoFetchResult))
        }
    }
    return arrPhotoAssetsData
}

func getDate(forDay day: Int, forMonth month: Int, forYear year: Int, forHour hour: Int, forMinute minute: Int, forSecond second: Int) -> Date? {
    var dateComponents = DateComponents()
    dateComponents.day = day
    dateComponents.month = month
    dateComponents.year = year
    dateComponents.hour = hour
    dateComponents.minute = minute
    dateComponents.second = second
    var gregorian = Calendar(identifier: Calendar.Identifier.gregorian)
    gregorian.timeZone = NSTimeZone.system
    return gregorian.date(from: dateComponents)
}

The code works nicely! But the problem is it takes almost 7 - 9 seconds to load 10k+ photos. Till 6k photos there is no problem, but I really need some efficient way so that I can load some of the asset in UICollectionView and rest of them I can add later. I need that no matter the photos count, it should not take more than 2 - 3 seconds. Can anybody please help?

1

There are 1 answers

6
Lokesh SN On

Let's say you have 8k photos. So you iterate through two 'for' loops in order to get the arrCreationDate and arrPhotoAssets data(which is double the work needed)

Instead, you can try doing it through a single loop. Here's a rough way:-

    let assetsFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
    let fetchOptions = PHFetchOptions()
    var arrCreationDate = [Date]()
    var arrPhotoAssetsData = [AssetsData]()
    var arrDates = [String]()

     for index in 0..<assetsFetchResult.count {
        if let creationDate = assetsFetchResult[index].creationDate {
            let formattedDate = formatter.string(from: creationDate)
            if !arrDates.contains(formattedDate) {
                //You can convert the formattedDate to actual date here and do a check similar to this, do what you do in the other loop here too
                if(actualDate < actualDateOfTheFirstElementAtArray){
                   arrCreationDate.insert(actualDate, at: 0)
                   fetchOptions.predicate = NSPredicate(format: "creationDate > %@ AND creationDate < %@", startDate as NSDate, endDate as NSDate)
                   let assetsPhotoFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
                   arrPhotoAssetsData.insert(AssetsData(creationDate: createdDate, assetResult: assetsPhotoFetchResult), at: 0)
                }
            }
        }
     }

This is just for you to get a rough idea of what I'm talking about, as this will reduce half the burden(just a single loop)

Also try using prefetchDataSource for your collection view to preload it with some data

EDIT:-

I assume that you have tried the following already:-

        func fetchPhotos() -> [AssetsData] {
    //Date Formatter
    let formatter = DateFormatter()
    formatter.dateStyle = DateFormatter.Style.medium
    formatter.timeStyle = DateFormatter.Style.none

    //Photos fetch
    let fetchOptions = PHFetchOptions()
    let sortOrder = [NSSortDescriptor(key: "creationDate", ascending: false)]
    fetchOptions.sortDescriptors = sortOrder
    let assetsFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
    var arrCreationDate = [Date]()
    var arrDates = [String]()
    var arrPhotoAssetsData = [AssetsData]()

    //Getting All dates
    for index in 0..<assetsFetchResult.count {
        if let creationDate = assetsFetchResult[index].creationDate {
            let formattedDate = formatter.string(from: creationDate)
            if !arrDates.contains(formattedDate) {
                arrDates.append(formattedDate)
                arrCreationDate.append(creationDate)
                convertToAssetsDataAndAppend(date: creationDate, fetchOptions: fetchOptions, toArray: &arrPhotoAssetsData)
            }
        }
    }
    return arrPhotoAssetsData
    }

    func convertToAssetsDataAndAppend(date: Date, fetchOptions: PHFetchOptions, toArray: inout [AssetsData]){
    if let startDate = getDate(forDay: date.day, forMonth: date.month, forYear: date.year, forHour: 0, forMinute: 0, forSecond: 0), let endDate = getDate(forDay: date.day, forMonth: date.month, forYear: date.year, forHour: 23, forMinute: 59, forSecond: 59) {
        fetchOptions.predicate = NSPredicate(format: "creationDate > %@ AND creationDate < %@", startDate as NSDate, endDate as NSDate)
        let assetsPhotoFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
        toArray.append(AssetsData(creationDate: date, assetResult: assetsPhotoFetchResult))
    }
}

func getDate(forDay day: Int, forMonth month: Int, forYear year: Int, forHour hour: Int, forMinute minute: Int, forSecond second: Int) -> Date? {
    var dateComponents = DateComponents()
    dateComponents.day = day
    dateComponents.month = month
    dateComponents.year = year
    dateComponents.hour = hour
    dateComponents.minute = minute
    dateComponents.second = second
    var gregorian = Calendar(identifier: Calendar.Identifier.gregorian)
    gregorian.timeZone = NSTimeZone.system
    return gregorian.date(from: dateComponents)
}

If this doesn't help, how about reloading the collection view with some kind of callback after every loop iteration? (with the above approach) This way, you won't make the user wait until the whole thing gets loaded

Idk, these might look petty but I'm just trying to help :)