Created
April 18, 2024 21:17
-
-
Save samsonjs/d8a93461035c87c39482a7c39e506568 to your computer and use it in GitHub Desktop.
Fetching assets from the photo library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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