iOS Realm grouping by date to tableView sections - ios

I have realm database, which contains data and date of adding this data. I want to exctract this and set date as table view section header and data as rows data for each section depend on date. I know how to exctract but dont know how to group by date and set data for each section depend on date. Thank you!

Swift 4 implementation using higher order functions rather then loops.
class Item: Object {
#objc dynamic var id: Int = 0
#objc dynamic var date: Date = Date()
}
let realm = try! Realm()
// fetch all Items sorted by date
let results = realm.objects(Item.self).sorted(byKeyPath: "date", ascending: false)
let sections = results
.map { item in
// get start of a day
return Calendar.current.startOfDay(for: item.date)
}
.reduce([]) { dates, date in
// unique sorted array of dates
return dates.last == date ? dates : dates + [date]
}
.compactMap { startDate -> (date: Date, items: Results<Item>) in
// create the end of current day
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
// filter sorted results by a predicate matching current day
let items = results.filter("(date >= %#) AND (date < %#)", startDate, endDate)
// return a section only if current day is non-empty
return items.isEmpty ? nil : (date: startDate, items: items)
}

You can just sort your retrieved Results by date and then split them up while iterate through to make them accessible in a grouped / hierarchic manner.
class Person {
dynamic var name = ""
dynamic var date = NSDate()
}
let sortedObjects = realm.objects(Person).sorted("date")
var lastDate = objects.first?.date
let calendar = NSCalendar.currentCalendar()
var lastGroup = [Person]()
var groups = [[Person]]()
for element in sortedObjects {
let currentDate = element.date
let difference = calendar.components([.Year, .Month, .Day], fromDate: lastDate!, toDate: currentDate, options: [])
if difference.year > 0 || difference.month > 0 || difference.day > 0 {
lastDate = currentDate
groups.append(lastGroup)
lastGroup = [element]
} else {
lastGroup.append(element)
}
}
groups.append(lastGroup)
Note: In that way, you would need to keep all your elements in memory. If that shouldn't work out for you, depending on your use-case, you could memorize only the indexes instead, which you can use to access the element from the retrieved Results.

I had the exact same issue, I needed to display one kind of Realm entities in a sectioned table, grouped by date, and this is how I did it.
Example class containing the date field:
final class Appointment: Object {
#objc dynamic var id: Int = 0
#objc dynamic var date: Date?
}
Example code that will get all objects and split them in sections/results, grouped by unique date:
// (un)safely get an instance of Realm
let realm = try! Realm()
// get all the dates
// note that begginingOfDay is a extension on Date
// which gives back the beggining of the day of the given Date as a Date
// we are doing this in order to filter out non-unique dates later
let dates = self.realm.objects(Appointment.self).toArray().flatMap({ $0.date ?? nil }).map({ $0.beginningOfDay() })
// cast it to a Set to make values unique, and back to an Array for further use
let uniqueDates = Array(Set(dates))
let predicates = uniqueDates.map({ date -> NSPredicate in
// in order to use Swift's Date with NSPredicate
// it must be casted to NSDate
let begginingOfDay = date.beginningOfDay() as NSDate
let endOfDay = date.endOfDay() as NSDate
// create a predicate that checks if the given Date is in between
// the beggining of a given Date and the end of the given Date
let predicate = NSPredicate(format: "(date >= %#) AND (date <= %#)", begginingOfDay, endOfDay)
return predicate
})
// create an array of Results<Appointment>, and then use it to drive your table/collection view
// I will leave this part to you, depends on your UI implementation
// personally, I wrap this into another object that contains results, section index, section title, etc.
// and then I use all of that in my table view's data source methods
let sectionedResults: [Results<Appointment>] = predicates.map({ predicate -> Results<Appointment> in
let results = realm.objects(Appointment.self).filter(predicate)
return results
})
You should now have a rough idea how to do it know, I'll leave the details of the UI implementation to you.

If someone is fighting with Swift 3.0 syntax:
var lastDate = dateObjects.first?.start
let calendar = Calendar.current
var lastGroup = [DateObject]()
var days = [[DateObject]]()
for dateObject in dateObjects {
let currentDate = dateObject.start
let unitFlags : Set<Calendar.Component> = [.era, .day, .month, .year, .timeZone]
let difference = calendar.dateComponents(unitFlags, from: lastDate!, to: currentDate)
if difference.year! > 0 || difference.month! > 0 || difference.day! > 0 {
lastDate = currentDate
days.append(lastGroup)
lastGroup = [travelTime]
} else {
lastGroup.append(dateObject)
}
}
days.append(lastGroup)

Related

Sort custom objects based on comparison between values of 2 different attributes

I have a custom object class as below which consists of 3 variables:
class DateObj {
var startDate: Date?
var endDate: Date?
var updatedEndDate: Date?
}
Below are the objects I created for it:
let obj1 = DateObj.init(startDate: 1/13/2022 7:00am, endDate: 1/13/2022 6:30pm, updatedEndDate: nil)
let obj2 = DateObj.init(startDate: 1/13/2022 10:30am, endDate: 1/14/2022 10:30am, updatedEndDate: 1/13/2022 10:30pm)
let obj3 = DateObj.init(startDate: 1/13/2022 11:30am, endDate: 1/14/2022 11:30am, updatedEndDate: 1/13/2022 7:30pm)
let obj4 = DateObj.init(startDate: 1/13/2022 1:30pm, endDate: 1/13/2022 5:30pm, updatedEndDate: nil)
Doesn't matter what the start date is, I want to compare values of endTime with updatedEndTime and want to sort such that the event that ends first (end time could be in endDate or updatedEndDate) should be first in the array and the event that ends last should be last in the array.
Note: updatedEndTime will always be less than endTime since the event could have ended earlier than expected time.
var inputDates = [obj1, obj2, obj3, obj4]
var expectedOutputDates = [obj4, obj1, obj3, obj2] // Expected output after sort
Code that I tried:
inputDates.sorted { (lhs, rhs) in
if let lhsUpdated = lhs.updatedEndDate, let rhsUpdated = rhs.updatedEndDate {
return lhsUpdated < rhsUpdated
} else if let lhsUpdated = lhs.updatedEndDate, let rhsEndTime = rhs.endDate {
return lhsUpdated < rhsEndTime
} else if let lhsEndTime = lhs.endDate, let rhsUpdated = rhs.updatedEndDate {
return lhsEndTime < rhsUpdated
} else if let lhsEndTime = lhs.endDate, let rhsEndTime = rhs.endDate {
return lhsEndTime < rhsEndTime
}
return false
}
My code is not giving me the expected output. Could someone help and let me know how to compare values from 2 different attributes for sorting an array?
Thanks!

Realm filter firstDate older then newDate

I am trying to get the objects from realm, where newDate is later then firstDate. So if the date from firstDate is 05.10.2017, it will get objects after that date, example 06.10.2017, but not 04.10.2017.
This is how I am storing the date:
class User: Object {
#objc dynamic var firstDate = Date()
#objc dynamic var newDate = Date()
}
This is how I am saving the objects:
let date = Date()
let realm = try! Realm()
let myUser = User()
myUser.firstDate = self.date
This is how I am trying to retrieve the objects:
var userData: Results<User>?
if (homeIndexPathRow == 0) {
let getData = realm.objects(User.self).filter("firstDate > newDate")
userData = getData
print("userData", userData!)
}
When trying to retrieve the objects, the app crashes.. Is it something wrong with the filter format?
Try this:
var yourNSDate = NSDate()
let predicate = NSPredicate(format: "firstDate < %#", yourNSDate)
let dataResults = realm.objects(User.self).filter(predicate)
userData = dataResults
Replace that with the code below if (homeIndexPathRow == 0) { ...

Swift 2 - remove NSDate values in array if value is before current date

In my Application I create an array of "NSDate" in order to send local notifications.
The values saved are "UUID" and "deadline" and they are saved using let gameDictionary = NSUserDefaults.standardUserDefaults().dictionaryForKey(GAME_INFO) ?? [:]
The result is somenting similar to this:
[{
UUID = "546C5E4D-CFEE-42F3-9010-9936753D17D85";
deadline = "2015-12-25 15:44:26 +0000";
}, {
UUID = "7C030614-C93C-4EB9-AD0A-93096848FDC7A";
deadline = "2015-12-25 15:43:15 +0000";
}]
What I am trying to achieve is to compare the "deadline" values with the current date and if the deadline is before than current date the values need to be removed from the array.
func compareDeadline() {
let gameDictionary = NSUserDefaults.standardUserDefaults().dictionaryForKey(GAME_INFO) ?? [:]
var items = Array(gameDictionary.values)
for i in 0..<items.count {
let dateNotification = items[i]["deadline"]!! as! NSDate
print(dateNotification)
var isOverdue: Bool {
return (NSDate().compare(dateNotification) == NSComparisonResult.OrderedDescending) // deadline is earlier than current date
}
print(isOverdue)
if (isOverdue == true){
items.removeAtIndex(i)
}
}
}
When I try to remove the values from the array I get Fatal Error: Array index out of range
Any Idea How can I solve this?
You should use the .filter method on the array to remove anything that you don't want in that array. The result is a new array with just the filtered results.
.filter requires you to set the filter criteria in a closure that you send into it
Here is a good article on how to use it
You can use filter method of swift array
For example to filter even numbers in array:
func isEven(number: Int) -> Bool {
return number % 2 == 0
}
evens = Array(1...10).filter(isEven)
println(evens)
There are a few problems. The reason you are getting an error is because you cannot remove elements while iterating inside for-in block. You can filter the items with the following code:
func compareDeadline() {
let gameDictionary = NSUserDefaults.standardUserDefaults().dictionaryForKey(GAME_INFO) ?? [:]
let items = Array(gameDictionary.values)
let currentDate = NSDate()
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ssZ"
let filteredItems = items.flatMap({
guard let stringDeadline = $0["deadline"] as? String, let deadline = dateFormatter.dateFromString(stringDeadline) else {
return nil
}
return deadline
}).filter({
currentDate.compare($0) == .OrderedDescending
})
}

How to get the latest date from array in Swift

I have an array of dates. I need to find the latest one. Can someone show me an example?
You can make NSDate conform to Comparable, as shown here
Given this, you can then use maxElement to find the maximum (i.e. the latest).
import Foundation
extension NSDate: Comparable { }
public func ==(lhs: NSDate, rhs: NSDate) -> Bool {
return lhs.isEqualToDate(rhs)
}
public func <(lhs: NSDate, rhs: NSDate) -> Bool {
return lhs.compare(rhs) == .OrderedAscending
}
let dates = [NSDate(), NSDate()]
let maxDate = maxElement(dates)
Note, maxElements goes bang for empty arrays so you may want to guard it with isEmpty:
let maxDate = dates.isEmpty ? nil : Optional(maxElement(dates))
Or, if you don’t want to go the extension route:
if let fst = dates.first {
let maxDate = dropFirst(dates).reduce(fst) {
$0.laterDate($1)
}
}
or, to return an optional:
let maxDate = dates.reduce(nil) {
(lhs: NSDate?, rhs: NSDate?)->NSDate? in
lhs.flatMap({rhs?.laterDate($0)}) ?? rhs
}
You can make use of reduce:
guard let dates = dates, !dates.isEmpty else { return nil }
dates.reduce(Date.distantPast) { $0 > $1 ? $0 : $1 }
Edit: Handle empty or nil array
Swift has Array methods for getting both the min and max values for dates.
You can use the following:
let maxDate = myArrayOfDates.max()
let minDate = myArrayOfDates.min()
So if you have an array of dates like so:
And here is the code if you want to copy it:
let now = Date()
let dates = [
now,
now.addingTimeInterval(120),
now.addingTimeInterval(60)
]
let sut = dates.max()
print(sut!)
Hope this helps someone!
Run this in your playground
var dates = [NSDate]()
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
let date1 = dateFormatter.dateFromString("02-06-1987")
let date2 = dateFormatter.dateFromString("02-06-2001")
let date3 = dateFormatter.dateFromString("02-06-2010")
//var date1 = NSDate()
dates.append(date3!)
dates.append(date1!)
dates.append(date2!)
var maxDate = dates[0]
for i in 0...dates.count-1
{
if(maxDate.compare(dates[i]).rawValue == -1){
maxDate = dates[i]
println(maxDate)
}
println(maxDate)
}
println(maxDate.description)
have a good day :)

iOS Swift: Sort array into multidimensional array

I have an array of CKRecords. Each record has startTime and a Name, among other values. What I would like to do is sort the records first by unique startTime and then within each startTime sort by unique Name.
The end result would be an array that looks like this (I think): records = [Date: [Name: [CKRecord]]]
Here is what I have right now:
func buildIndex(records: [CKRecord]) -> [[CKRecord]] {
var dates = [NSDate]()
var result = [[CKRecord]]()
for record in records {
var date = record.objectForKey("startTime") as! NSDate
if !contains(dates, date) {
dates.append(date)
}
}
for date in dates {
var recordForDate = [CKRecord]()
for (index, exercise) in enumerate(exercises) {
let created = exercise.objectForKey("startTime") as! NSDate
if date == created {
let record = exercises[index] as CKRecord
recordForDate.append(record)
}
}
result.append(recordForDate)
}
return result
}
let records = self.buildIndex(data)
Why not use sorted? Like this.
// A simplified version of your `CKRecord` just for demonstration
struct Record {
let time: NSDate
let name: String
}
let records = [
Record(time: NSDate(timeIntervalSince1970: 1), name: "a"),
Record(time: NSDate(timeIntervalSince1970: 2), name: "b"),
Record(time: NSDate(timeIntervalSince1970: 1), name: "c"),
Record(time: NSDate(timeIntervalSince1970: 3), name: "d"),
Record(time: NSDate(timeIntervalSince1970: 3), name: "e"),
Record(time: NSDate(timeIntervalSince1970: 2), name: "f"),
]
func buildIndex(records: [Record]) -> [[Record]] {
var g = [NSDate: [Record]]()
for e in records {
if (g[e.time] == nil) {
g[e.time] = []
}
g[e.time]!.append(e) // grouping by `time`
}
return sorted(g.keys) { (a: NSDate, b: NSDate) in
a.compare(b) == .OrderedAscending // sorting the outer array by 'time'
}
// sorting the inner arrays by `name`
.map { sorted(g[$0]!) { $0.name < $1.name } }
}
println(buildIndex(records))
First of all, you're not really trying to sort an array here, you're trying to order a dictionary, which isn't built to be iterated over sequentially. In fact even if you do sort the array first and then build the dictionary like this:
var sortedRecords = [NSDate: [String: CKRecord]]()
records.sort { return $0.date.timeIntervalSinceDate($1.date) < 0 }
for record in records {
if sortedRecords[record.date] != nil {
sortedRecords[record.date] = [String: CKRecord]()
}
sortedRecords[record.date]![record.name] = record
}
The order isn't guaranteed when you iterate over it in the future. That said, a dictionary is essentially a look up table, and elements can be accessed in O(log n) time. What you'll really want to do is either drop the dictionary is favor of an array of [CKRecord] and then sort like this:
records.sort { $0.date.timeIntervalSinceDate($1.date) == 0 ? $0.name < $1.name : $0.date.timeIntervalSinceDate($1.date) < 0 }
Or, depending on what your end goal is, iterate across a range of dates, plucking the entries from the dictionary as you go.
You could execute the CloudKit query and make sure that you get the array returned in the correct sort order like this:
query.sortDescriptors = [NSSortDescriptor(key: "startTime", ascending: true), NSSortDescriptor(key: "Name", ascending: true)]
And then if you go to the detail view, you could use the filter for getting the records for that day like this:
var details = records.filter { (%0.objectForKey("startTime") As! NSDate) == selectedDate }

Resources