1

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?

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
Sohil R. Memon
  • 9,404
  • 1
  • 31
  • 57

1 Answers1

1

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 :)

Lokesh SN
  • 1,583
  • 7
  • 23
  • Thanks. I tried but it still taking 2.09 seconds to fetch 3.1k photos while mine is taking 1.85 seconds. – Sohil R. Memon Nov 01 '18 at 12:05
  • Cool, so you've used another loop inside the current for loop to sort the elements? In that case, it's a nested loop and would obviously consume more time than usual two separate loops. What you can do is to try to sort using binary search – Lokesh SN Nov 02 '18 at 06:27
  • Umm..Binary search won't be efficient that way, as what I think because there is no search for specific date. We need all the data as per the dates fetch from the library. what do you think? – Sohil R. Memon Nov 02 '18 at 09:22
  • No, I get that, but I assumed you followed an approach similar to what I thought, my bad. I've edited the answer. Let me know what you think... – Lokesh SN Nov 02 '18 at 12:12
  • So you have just differentiate the appending part! That's cool, I will try that way like first 5 or 6 dates are appended then reload `UICollectionView` and then again appending new data reload it. – Sohil R. Memon Nov 02 '18 at 12:46
  • Yep, exactly what I mean! – Lokesh SN Nov 02 '18 at 15:34