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

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?

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

Related

HealthKit Fetch Workout Time, Distance, Avg Heart Rate

I was able to fetch some of the details separated from HealthKit app, but i have a missing points that i need help with such as getting the below exact data for each workout.
And here is what i tried for Exercise time:
func getExerciseTime(forDate date: Date, _ completion: ((Double, Error?) -> Void)!) {
let cal = Calendar.current
let startDate = cal.startOfDay(for: date)
var comps = DateComponents()
comps.day = 1
comps.second = -1
let endDate = cal.date(byAdding: comps, to: startDate)
let stepsCount = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.appleExerciseTime)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let interval: NSDateComponents = NSDateComponents()
interval.day = 1
let query = HKStatisticsCollectionQuery(quantityType: stepsCount!, quantitySamplePredicate: predicate, options: [.cumulativeSum], anchorDate: startDate as Date, intervalComponents: interval as DateComponents)
query.initialResultsHandler = { query, results, error in
if error != nil {
// Something went Wrong
return
}
var exercise = 0.0
if let myResults = results, let endDate = endDate {
myResults.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
if let quantity = statistics.sumQuantity() {
exercise = quantity.doubleValue(for: HKUnit.minute())
}
}
}
completion(round(exercise), error)
}
healthKitStore.execute(query)
}
I was able to get the beats but not the average heart rate for the workout:
func getAverageHeartRate(date:Date,completion:
#escaping ([HKWorkout]?, Error?) -> Void) {
guard let heartRateType =
HKObjectType.quantityType(forIdentifier:
HKQuantityTypeIdentifier.heartRate) else {
fatalError("*** Unable to create a Heart rate type ***")
}
let cal = Calendar.current
let startDate = cal.startOfDay(for: date)
var comps = DateComponents()
comps.day = 1
comps.second = -1
let endDate = cal.date(byAdding: comps, to: startDate)
// Get all workouts that only came from this app.
let workoutPredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let startDateSort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let query = HKSampleQuery(sampleType: heartRateType,
predicate: workoutPredicate,
limit: 0,
sortDescriptors: [startDateSort]) { (sampleQuery, results, error) -> Void in
guard let heartRateSamples = results as? [HKQuantitySample] else {
// Perform proper error handling here.
return
}
// Use the workout's Heart rate samples here.
for heartrate in heartRateSamples {
let beats: Double? = heartrate.quantity.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute()))
print(heartrate.startDate, heartrate.endDate)
print(beats)
guard beats != nil else {
return
}
}
}
healthKitStore.execute(query)
And lastly:
func getWorkOutData(forDate date: Date, _ completion: ((Int, Error?) -> Void)!) {
let cal = Calendar.current
let startDate = cal.startOfDay(for: date)
var comps = DateComponents()
comps.day = 1
comps.second = -1
let endDate = cal.date(byAdding: comps, to: startDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: HKQueryOptions())
let sampleQuery = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: predicate, limit: 0, sortDescriptors: [sortDescriptor]) { (_, results, error ) -> Void in
var eligible = 0
if let myResults = results as? [HKWorkout] {
for workout in myResults where workout.duration >= 600 {
eligible += 1
}
}
completion(eligible, error)
}
healthKitStore.execute(sampleQuery)
}
1- I was able to get the heartbeats not the avg heart rate per workout.
2- I couldn't connect the data from each one to be like a workout model that contains Time,Distance,AvgHeart rate.

swift Ekevents to 2 dimensional array sorted by Date

I have an array with events which are sorted by date. I want a separate section for each day [monday], [tuesday], ....
How do I create a 2-dimensional array that looks like this?
And how do i get each event in the appropriate section?
[
[monday]
[tuesday]
[wednesday]
. .and so on ]
func getEvents(){
let calendars = eventStore.calendars(for: .event)
for calendar in calendars {
// if calendar.title == "Arbeit" {
let anfang = NSDate()
let ende = Calendar.current.date(byAdding: .day, value: 6, to: anfang as Date)!
let predicate = eventStore.predicateForEvents(withStart: anfang as Date, end: ende as Date, calendars: [calendar])
let events = eventStore.events(matching: predicate)
var found:Bool = false
for event in events {
if (allEvents.count == 0)
{
allEvents.append(event)
}
if (allEvents.count > 0)
{
found = false
if ( event.title == allEvents[allEvents.count-1].title && event.startDate == allEvents[allEvents.count-1].startDate)
{
found = true
}
}
if(!found)
{
allEvents.append(event)
}
}
The following solution first groups the events based start date by weekday in a dictionary and then transforms the dictionary to a 2D array sorted with todays day first
First set up a Calendar and DateFormatter to use
let locale = Locale(identifier: "en_US_POSIX")
var calendar = Calendar.current
calendar.locale = locale
let formatter = DateFormatter()
formatter.locale = locale
formatter.dateFormat = "EEEE"
Group the events
let byWeekday = Dictionary(grouping: events, by: { formatter.string(from: $0.startDate) })
Get the weekdays in the right order
let weekdays = calendar.weekdaySymbols
let todayIndex = weekdays.firstIndex(of: formatter.string(from: Date())) ?? calendar.weekdaySymbols.startIndex
let sortedWeekdays = weekdays[todayIndex..<weekdays.endIndex] + weekdays[weekdays.startIndex..<todayIndex]
and then create and fill the array
var eventsByDay = [[EKEvent]]()
for day in sortedWeekdays {
eventsByDay.append(byWeekday[day] ?? [])
}

How do i display CoreData data from the past week or month?

I have data saved as meals; Breakfast, Lunch, Dinner and Snacks. I am trying to return a total for the calories from all meals for each day over the previous week.
The meal data is saved as below with .date :
func saveBreakfast() {
let newBreakfastItem = BreakfastItem(context: self.moc)
newBreakfastItem.id = UUID()
newBreakfastItem.name = self.item.name
newBreakfastItem.calories = Int32(self.totalCalories)
newBreakfastItem.carbs = Int32(self.totalCarbs)
newBreakfastItem.protein = Int32(self.totalProtein)
newBreakfastItem.fat = Int32(self.totalFats)
newBreakfastItem.date = self.dateAdded
do {
if self.mocB.hasChanges { // saves only if changes are made
try? self.mocB.save()
}
}
}
func saveLunch() {
let newLunchItem = LunchItem(context: self.moc)
newLunchItem.id = UUID()
newLunchItem.name = self.item.name
newLunchItem.calories = Int32(self.caloriesPerServing)
newLunchItem.carbs = Int32(self.carbsPerServing)
newLunchItem.protein = Int32(self.proteinPerServing)
newLunchItem.fat = Int32(self.fatsPerServing)
newLunchItem.date = self.dateAdded
do {
if self.mocL.hasChanges {
try? self.mocL.save()
}
}
}
I am currently working with the below function to try and get it working for a single day (Date() - 1 day, so yesterday)
func dayOneCal(at date: Date) -> NSNumber {
let request1:NSFetchRequest<BreakfastItem> = BreakfastItem.fetchRequest() as! NSFetchRequest<BreakfastItem>
let request2:NSFetchRequest<LunchItem> = LunchItem.fetchRequest() as! NSFetchRequest<LunchItem>
let request3:NSFetchRequest<DinnerItem> = DinnerItem.fetchRequest() as! NSFetchRequest<DinnerItem>
let request4:NSFetchRequest<SnackItem> = SnackItem.fetchRequest() as! NSFetchRequest<SnackItem>
let startDate = Calendar.current.startOfDay(for: date)
var components = DateComponents()
components.day = -1
components.second = -1
let endDate = Calendar.current.date(byAdding: components, to: startDate)!
request1.predicate = NSPredicate(format: "date >= %# AND date <= %#", startDate as NSDate, endDate as NSDate)
request2.predicate = NSPredicate(format: "date >= %# AND date <= %#", startDate as NSDate, endDate as NSDate)
request3.predicate = NSPredicate(format: "date >= %# AND date <= %#", startDate as NSDate, endDate as NSDate)
request4.predicate = NSPredicate(format: "date >= %# AND date <= %#", startDate as NSDate, endDate as NSDate)
let totalDailyBreakfastCals = BreakfastItems.map({$0.calories}).reduce(0, +)
let totalDailyLunchCals = LunchItems.map({$0.calories}).reduce(0, +)
let totalDailyDinnerCals = DinnerItems.map({$0.calories}).reduce(0, +)
let totalDailySnacksCals = SnackItems.map({$0.calories}).reduce(0, +)
let totalDailyCals = totalDailyBreakfastCals + totalDailyLunchCals + totalDailyDinnerCals + totalDailySnacksCals
return totalDailyCals as NSNumber
}
Any help would be greatly appreciated, thank you in advance!

Calculate age from birth date using textfield and NSDateComponents in Swift4

I am trying calculate the age from birthday Date in Swift with this function: (want to write in a textField and pass this data from VC in a Label)
{
var a = self.dob.text
var c = a!.components(separatedBy: "-")
var y1 = c[2]
let cal = NSCalendar? = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let year = Calendar.components(.year, from: dob!, to: now, options: [])
let age = (year!) - Int(y1)!
self.myage.text = String(age)
}
But I get an error cannot assign NSCalendar?.Type, but I don't know why get this error (its my first time coding)
You have a few problems in your code. First there is a type as already mentioned by Qi Hao, second you are passing dob is a text field you and Calendar components method expects two dates, so you should first parse the text field date then you can get the year component difference from input date and now:
Playground Testing
let dob = UITextField()
dob.text = "03-27-2002"
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "MM-dd-yyyy"
if let date = dateFormatter.date(from: dob.text!) {
let age = Calendar.current.dateComponents([.year], from: date, to: Date()).year!
print(age) // 16
}
func age(on baseDate: DateComponents) -> Int {
if baseDate.month > month {
return baseDate.year - year
}
if baseDate.month == month && baseDate.day >= day {
return baseDate.year - year
}
return baseDate.year - year - 1
}
try this:
func calcAge(birthday: String) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
let birthdayDate = dateFormater.date(from: birthday)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthdayDate!, to: now, options: [])
let age = calcAge.year
return age!
}

Working out the start and end of a day. Swift

I have a function to work out the start and end of a week which works as expected. I want to implement another function which works out the start and end of a single day. I have the code below however I get the following error:
Type of expression is ambiguous without more context.
public class Date {
let dateFormatter = NSDateFormatter()
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
func calcStartAndEndDateForWeek(durationOccurance: Double) {
print("Calculating start and end for week")
let componentsWeek = calendar.components([.YearForWeekOfYear, .WeekOfYear], fromDate: date)
let startOfWeek = calendar.dateFromComponents(componentsWeek)!
print("start of Week = \(dateFormatter.stringFromDate(startOfWeek))")
let componentsWeekEnds = NSDateComponents()
componentsWeekEnds.weekOfYear = 1
let endOfWeek = calendar.dateByAddingComponents(componentsWeekEnds, toDate: startOfWeek, options: [])!
print("End of the week = \(dateFormatter.stringFromDate(endOfWeek))")
}
func calcStartAndEndDateForDay(durationOccurance: Double) {
print("Calculating start and end for day")
let componentsWeek = calendar.components([.Minutes, .Seconds], fromDate: date)
let startOfDay = calendar.dateFromComponents(componentsWeek)!
print("start day = \(dateFormatter.stringFromDate(startOfDay))")
}
init(){
dateFormatter.dateFormat = "dd-MM-yyyy"
}
}
We can create a more generic function using the methods on NSCalendar:
func rangeOfPeriod(period: NSCalendarUnit, date: NSDate) -> (NSDate, NSDate) {
let calendar = NSCalendar.currentCalendar()
var startDate: NSDate? = nil
// let's ask calendar for the start of the period
calendar.rangeOfUnit(period, startDate: &startDate, interval: nil, forDate: date)
// end of this period is the start of the next period
let endDate = calendar.dateByAddingUnit(period, value: 1, toDate: startDate!, options: [])
// you can subtract 1 second if you want to make "Feb 1 00:00:00" into "Jan 31 23:59:59"
// let endDate2 = calendar.dateByAddingUnit(.Second, value: -1, toDate: endDate!, options: [])
return (startDate!, endDate!)
}
Called as
print("\(rangeOfPeriod(.WeekOfYear, date: NSDate()))")
print("\(rangeOfPeriod(.Day, date: NSDate()))")
Putting it into your code:
public class Date {
let dateFormatter = NSDateFormatter()
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
func rangeOfPeriod(period: NSCalendarUnit) -> (NSDate, NSDate) {
var startDate: NSDate? = nil
calendar.rangeOfUnit(period, startDate: &startDate, interval: nil, forDate: date)
let endDate = calendar.dateByAddingUnit(period, value: 1, toDate: startDate!, options: [])
return (startDate!, endDate!)
}
func calcStartAndEndDateForWeek() {
let (startOfWeek, endOfWeek) = rangeOfPeriod(.WeekOfYear)
print("Start of week = \(dateFormatter.stringFromDate(startOfWeek))")
print("End of the week = \(dateFormatter.stringFromDate(endOfWeek))")
}
func calcStartAndEndDateForDay() {
let (startOfDay, endOfDay) = rangeOfPeriod(.Day)
print("Start of day = \(dateFormatter.stringFromDate(startOfDay))")
print("End of the day = \(dateFormatter.stringFromDate(endOfDay))")
}
init() {
dateFormatter.dateFormat = "dd-MM-yyyy"
}
}
let myDate = Date()
myDate.calcStartAndEndDateForWeek()
myDate.calcStartAndEndDateForDay()
I was implementing something similar and went the following route:
extension Date {
static var startOfToday: Date? {
let date = Date()
guard !date.isStartOfDay else { return date }
return date
.zero(out: .second)?
.zero(out: .minute)?
.zero(out: .hour)?
.addingTimeInterval(-24 * 60 * 60)
}
private func zero(out: Calendar.Component) -> Date? {
return Calendar.current
.date(bySetting: out, value: 0, of: self)
}
private var isStartOfDay: Bool {
let cal = Calendar.current
let hours = cal.component(.hour, from: self)
let minutes = cal.component(.minute, from: self)
let seconds = cal.component(.second, from: self)
return hours == 0 && minutes == 0 && seconds == 0
}
}
Setting a component to zero will increment the next bigger component. So just setting the hour to zero will push the date to the next day at 00:00, unless of course the hour is already at zero. So to make it work for any date we have to zero out the seconds, minutes and hours (in that order). And to make sure we don't end up at the beginning of yesterday we first check if all values aren't already at zero.
I realize this is kinda hacky and probably not the best way to go about this, but it seems to work well enough for my use-case at least.
Getting the end of the day can be built on top of this by just adding another day.