Skip to content

Instantly share code, notes, and snippets.

@samsonjs
Created April 18, 2024 21:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samsonjs/d8a93461035c87c39482a7c39e506568 to your computer and use it in GitHub Desktop.
Save samsonjs/d8a93461035c87c39482a7c39e506568 to your computer and use it in GitHub Desktop.
Fetching assets from the photo library
//
// AssetFetcher.swift
// DailyDrip
//
// Created by Sami Samhuri on 2022-11-06.
//
import Foundation
import Photos
@MainActor
class AssetFetcher {
let calendar: Calendar = .current
func fetchAssets(
selectedDate: Date,
assetSourceTypes: PHAssetSourceType,
excludedMediaSubtypes: PHAssetMediaSubtype,
favesOnly: Bool
) -> PHFetchResult<PHAsset> {
let options = PHFetchOptions()
options.includeAssetSourceTypes = assetSourceTypes
options.predicate = predicateMatching(
selectedDate: selectedDate,
excludedMediaSubtypes: excludedMediaSubtypes,
favesOnly: favesOnly
)
return PHAsset.fetchAssets(with: options)
}
private var cachedOldestDate: Date?
// Caching isn't the biggest win here but it helps a bit.
private func oldestDate() -> Date {
guard cachedOldestDate == nil else { return cachedOldestDate! }
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
options.fetchLimit = 1
let result = PHAsset.fetchAssets(with: options)
cachedOldestDate = result.firstObject?.creationDate
return cachedOldestDate ?? Date()
}
private func predicateMatching(
selectedDate: Date,
excludedMediaSubtypes: PHAssetMediaSubtype,
favesOnly: Bool
) -> NSPredicate {
// Match every day with the selected date's month and day, from the first (oldest) year to
// the current year. This is very fast and means that we don't have to filter anything in
// code here, which tends to be rather slow.
let firstYear = calendar.component(.year, from: oldestDate())
let thisYear = calendar.component(.year, from: .now)
// Filter years to handle leap days properly. Date math is forgiving, but we don't want it
// to be forgiving. Filter out dates that don't have a matching month so we don't show
// results from 1st March on 29th Februrary.
var components = calendar.dateComponents([.year, .month, .day], from: selectedDate)
let startDates = (firstYear...thisYear).compactMap { year -> Date? in
components.year = year
guard let date = calendar.date(from: components) else {
return nil
}
return calendar.component(.month, from: date) == components.month ? date : nil
}
let creationDateRange = "(creationDate >= %@ AND creationDate < %@)"
var format = "(" +
Array(repeating: creationDateRange, count: startDates.count).joined(separator: " OR ") +
")"
let dates = startDates.flatMap { date -> [Date] in
let startOfDay = calendar.startOfDay(for: date)
let nextDay = calendar.date(byAdding: .day, value: 1, to: date)!
return [startOfDay, nextDay]
}
dump(dates)
var args: [Any] = Array(dates)
if favesOnly {
format += " AND favorite == true"
}
if !excludedMediaSubtypes.isEmpty {
// The double negative is required to make this actually work. I have no idea why.
format += " AND NOT (mediaSubtypes & %d) != 0"
args.append(excludedMediaSubtypes.rawValue)
}
return NSPredicate(format: format, argumentArray: args)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment