Best Practice - Sorting mixed objects in Swift - ios

I am trying to sort an array of different NSManagedObjects in Swift. In Objective-C, it would have required just 2 lines of code (a sort descriptor and the actual sort). However, the only way I could figure out how to do it in swift required several lines of code. Is there a better and/or faster way than the code I wrote please?:
var orderMOs = [NSManagedObject]()
orderMOs.append(contentsOf: incidentMOs)
orderMOs.append(contentsOf: transferMOs)
orderMOs.sort (by: {(leftMO, rightMO) -> Bool in
var leftDate: NSDate?
var rightDate: NSDate?
if leftMO is Incident {leftDate = (leftMO as! Incident).createdDate}
else if leftMO is Transfer {leftDate = (leftMO as! Transfer).createdDate}
if rightMO is Incident {rightDate = (rightMO as! Incident).createdDate}
else if rightMO is Transfer {rightDate = (rightMO as! Transfer).createdDate}
if leftDate == nil || rightDate == nil {return true}
return leftDate!.compare(rightDate! as Date) == .orderedDescending
})

You should both your classes conform to a protocol that declares createdDate. Type orderMOs as such. than you won't need the conditional casts.
import Foundation
class A {
init(created createdDate: Date) {
self.createdDate = createdDate
}
let createdDate: Date
}
class B {
init(created createdDate: Date) {
self.createdDate = createdDate
}
var createdDate: Date
}
protocol Created {
var createdDate: Date { get }
}
extension A: Created {}
extension B: Created {}
func createDate(year: Int, month: Int, day: Int) -> Date {
var comps = DateComponents()
comps.year = year
comps.month = month
comps.day = day
return Calendar.current.date(from: comps)!
}
var objects = [Created]()
objects.append(A(created: createDate(year: 2018, month: 2, day: 1)))
objects.append(B(created: createDate(year: 2017, month: 12, day: 1)))
objects.append(B(created: createDate(year: 2018, month: 5, day: 18)))
objects.append(A(created: Date()))
Sort it like
objects.sort { (c1, c2) -> Bool in
return c1.createdDate < c2.createdDate
}
Another thing you can do is to use switch-statement with pattern matching to clean code up a bit.

Why not use KVC here?
if let leftDate = leftMO.value(forKey: "createdDate") as? Date,
let rightDate = rightMO.value(forKey: "createdDate") as? Date {
if leftDate == nil || rightDate == nil {return true}
return leftDate.compare(rightDate) == . orderedDescending
}

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!

Save array of Days in Swift 5

So i have created a class for a Day and for a Drink. and I'm trying to track how much you drink in a day, but I'm struggling with saving multiple days. I'm currently managing to save the current day(with the amount drunk that day) but i don't know how to save more than one day.
I want to save an array of type Day with all the days. how can i do this?
This is my Day class:
public class Day: NSObject {
var date: Date
var goalAmount: Drink
var consumedAmount: Drink
func saveDay() {
let formatting = DateFormatter()
formatting.dateFormat = "EEEE - dd/mm/yy"
UserDefaults.standard.set(formatting.string(from: date), forKey: "date")
UserDefaults.standard.set(goalAmount.amountOfDrink, forKey: "goal")
UserDefaults.standard.set(consumedAmount.amountOfDrink, forKey: "consumed")
}
func loadDay() {
let rawDate = UserDefaults.standard.value(forKey: "date") as? String ?? ""
let formatter = DateFormatter()
formatter.dateFormat = "EEEE - dd/mm/yy"
date = formatter.date(from: rawDate)!
goalAmount.amountOfDrink = UserDefaults.standard.float(forKey: "goal")
consumedAmount.amountOfDrink = UserDefaults.standard.float(forKey: "consumed")
}
}
This is my Drink class:
class Drink: NSObject {
var typeOfDrink: String
var amountOfDrink: Float
}
i am calling saveDay() when there are any changes made to the day, and then loadDay() when the app opens.
A better approach would be is to store the object of the class in userDefaults instead of storing particular properties of that class. And use [Date] instead of Date to save multiple days
For this first, you have Serialize the object to store in userDefaults and Deserialize to fetch the data from userDefaults.
import Foundation
class Day: Codable {
var date = Date()
var goalAmount: Drink
var consumedAmount: Drink
init(date: Date, goalAmount: Drink,consumedAmount: Drink ) {
self.date = date
self.goalAmount = goalAmount
self.consumedAmount = consumedAmount
}
static func saveDay(_ day : [Day]) {
do {
let object = try JSONEncoder().encode(day)
UserDefaults.standard.set(object, forKey: "days")
} catch {
print(error)
}
}
static func loadDay() {
let decoder = JSONDecoder()
if let object = UserDefaults.standard.value(forKey: "days") as? Data {
do {
let days = try decoder.decode([Day].self, from: object)
for day in days {
print("Date - ", day.date)
print("Goal Amount - ", day.goalAmount)
print("Consumed Amount - ",day.consumedAmount)
print("----------------------------------------------")
}
} catch {
print(error)
}
} else {
print("unable to fetch the data from day key in user defaults")
}
}
}
class Drink: Codable {
var typeOfDrink: String
var amountOfDrink: Float
init(typeOfDrink: String,amountOfDrink: Float ) {
self.typeOfDrink = typeOfDrink
self.amountOfDrink = amountOfDrink
}
}
Use saveAndGet() method to store and fetch details from userDefaults
func saveAndGet() {
// use any formats to format the dates
let date = Date()
let goalAmount = Drink(typeOfDrink: "Water", amountOfDrink: 5.0)
let consumedAmount = Drink(typeOfDrink: "Water", amountOfDrink: 3.0)
let day1 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let day2 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let day3 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let day4 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let days = [day1, day2, day3, day4]
Day.saveDay(days)
Day.loadDay()
}
1) You need to create array of object for this :
goalAmount = [Drink]()
var date = [Date]()
and append with each new element.
you can also add date variable inside your drink class.
2) you can also create array of dictionary:
var userData = [String : Any]()
key will be you date and Any contain related to drink data in Any you can store Anything.

Group Array to introduce sections to UITableView in Swift

I have the following class:
ChatMessage: Codable {
var id: Int?
var sender: User?
var message: String?
var seen: Int?
var tsp: Date?
}
The tsp is formatted like this: YYYY-MM-DD hh:mm:ss
I would like to "group" messages sent on the same day to end up with something like in this example:
let groupedMessages = [ [ChatMessage, ChatMessage], [ChatMessage, ChatMessage, ChatMessage] ]
I ultimately want to use groupedMessages in a UITableViewController to introduce sections like so:
func numberOfSections(in tableView: UITableView) -> Int {
return groupedMessages.count
// would be 2 int the above
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return chatMessages[section].count
// would be 2 for the first and 3 for the second section
}
What would be the most performant way of getting the sorting done? - i.e. something that also works well once the number of chatMessages to be sorted increases
You could try:
let cal = Calendar.current
let groupedMessages = Dictionary(grouping: self.chatMessages, by: { cal.startOfDay($0.tsp) })
let keys = groupedMessages.keys.sorted(by: { $0 > $1 })
This however would give you a dictionary like:
[
SomeDateHere: [
ChatMessage(id: 0, sender: User?, message: "String", seen: 1),
ChatMessage(id: 0, sender: User?, message: "String", seen: 1)
],
AnotherDateHere: [
ChatMessage(id: 0, sender: User?, message: "String", seen: 1)
]
You could then use keys to return the section count:
return keys.count
And to get the array of messages for each dictionary item like so:
let key = keys[indexPath.section]
let messages = groupedMessages[key].sorted(by: { $0.tsp > $1.tsp })
Or to get just the count:
let key = keys[indexPath.section]
return groupedMessages[key].count
Grouping array by date you could find here How to group array of objects by date in swift?
How to connect this to UITableView you will find here https://www.ralfebert.de/ios-examples/uikit/uitableviewcontroller/grouping-sections/
I would probably start by introducing a new type:
public struct CalendarDay: Hashable {
public let date: Date
public init(date: Date = Date()) {
self.date = Calendar.current.startOfDay(for: date)
}
}
extension CalendarDay: Comparable {
public static func < (lhs: CalendarDay, rhs: CalendarDay) -> Bool {
return lhs.date < rhs.date
}
}
Then I would extend your message:
extension ChatMessage {
var day: CalendarDay {
return CalendarDay(date: tsp)
}
}
(I am assumming tsp is not an optional, there is no reason for it to be optional).
Now, let's define a section:
struct Section {
var messages: [ChatMessage] = []
let day: CalendarDay
init(day: CalendarDay) {
self.day = day
}
}
Let's group easily:
let messages: [ChatMessage] = ...
var sections: [CalendarDay: Section] = [:]
for message in messages {
let day = message.day
var section = sections[day] ?? Section(day: day)
section.messages.append(day)
sections[day] = section
}
let sectionList = Array(sections.values).sorted { $0.day < $1.day }
If your messages are not originally sorted, you might need to also sort messages in every section separately.
let testArray = ["2016-06-23 09:07:21", "2016-06-23 08:07:21", "2016-06-22 09:07:21", "2016-06-22 08:07:21"]
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-DD hh:mm:ss"
let groupDic = Dictionary(grouping: testArray) { (pendingCamera) -> DateComponents in
let date = Calendar.current.dateComponents([.day, .year, .month], from: dateFormatter.date(from: pendingCamera)!)
return date
}
print(groupDic)
You can take groupDic and return in numberOfSections and titleForHeaderInSection.

How to dynamically create sections in a SwiftUI List/ForEach and avoid "Unable to infer complex closure return type"

I'm trying to recreate a view that matches the event list in the stock iOS Calendar app. I have the following code which generates a list of events with each event separated into its own section by Date:
var body: some View {
NavigationView {
List {
ForEach(userData.occurrences) { occurrence in
Section(header: Text("\(occurrence.start, formatter: Self.dateFormatter)")) {
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
}
}
.navigationBarTitle(Text("Events"))
}.onAppear(perform: populate)
}
The problem with this code is that if there are two events on the same date, they are separated into different sections with the same title instead of being grouped together into the same section.
As a Swift novice, my instinct is to do something like this:
ForEach(userData.occurrences) { occurrence in
if occurrence.start != self.date {
Section(header: Text("\(occurrence.start, formatter: Self.dateFormatter)")) {
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
} else {
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
self.date = occurrence.start
But in Swift, this gives me the error "Unable to infer complex closure return type; add explicit type to disambiguate" because I'm calling arbitrary code (self.date = occurrence.start) inside ForEach{}, which isn't allowed.
What's the correct way to implement this? Is there a more dynamic way to execute this, or do I need to abstract the code outside of ForEach{} somehow?
Edit: The Occurrence object looks like this:
struct Occurrence: Hashable, Codable, Identifiable {
var id: Int
var title: String
var description: String
var location: String
var start: Date
var end: String
var cancelled: Bool
var public_occurrence: Bool
var created: String
var last_updated: String
private enum CodingKeys : String, CodingKey {
case id, title, description, location, start, end, cancelled, public_occurrence = "public", created, last_updated
}
}
Update:
The following code got me a dictionary which contains arrays of occurrences keyed by the same date:
let myDict = Dictionary( grouping: value ?? [], by: { occurrence -> String in
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter.string(from: occurrence.start)
})
self.userData.latestOccurrences = myDict
However, if I try and use this in my View as follows:
ForEach(self.occurrencesByDate) { occurrenceSameDate in
// Section(header: Text("\(occurrenceSameDate[0].start, formatter: Self.dateFormatter)")) {
ForEach(occurrenceSameDate, id: occurrenceSameDate.id){ occurrence in
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
// }
}
(Section stuff commented out while I get the main bit working)
I get this error: Cannot convert value of type '_.Element' to expected argument type 'Occurrence'
In reference to my comment on your question, the data should be put into sections before being displayed.
The idea would be to have an array of objects, where each object contains an array of occurrences. So we simplify your occurrence object (for this example) and create the following:
struct Occurrence: Identifiable {
let id = UUID()
let start: Date
let title: String
}
Next we need an object to represent all the occurrences that occur on a given day. We'll call it a Day object, however the name is not too important for this example.
struct Day: Identifiable {
let id = UUID()
let title: String
let occurrences: [Occurrence]
let date: Date
}
So what we have to do is take an array of Occurrence objects and convert them into an array of Day objects.
I have created a simple struct that performs all the tasks that are needed to make this happen. Obviously you would want to modify this so that it matches the data that you have, but the crux of it is that you will have an array of Day objects that you can then easily display. I have added comments through the code so that you can clearly see what each thing is doing.
struct EventData {
let sections: [Day]
init() {
// create some events
let first = Occurrence(start: EventData.constructDate(day: 5, month: 5, year: 2019), title: "First Event")
let second = Occurrence(start: EventData.constructDate(day: 5, month: 5, year: 2019, hour: 10), title: "Second Event")
let third = Occurrence(start: EventData.constructDate(day: 5, month: 6, year: 2019), title: "Third Event")
// Create an array of the occurrence objects and then sort them
// this makes sure that they are in ascending date order
let events = [third, first, second].sorted { $0.start < $1.start }
// create a DateFormatter
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
// We use the Dictionary(grouping:) function so that all the events are
// group together, one downside of this is that the Dictionary keys may
// not be in order that we require, but we can fix that
let grouped = Dictionary(grouping: events) { (occurrence: Occurrence) -> String in
dateFormatter.string(from: occurrence.start)
}
// We now map over the dictionary and create our Day objects
// making sure to sort them on the date of the first object in the occurrences array
// You may want a protection for the date value but it would be
// unlikely that the occurrences array would be empty (but you never know)
// Then we want to sort them so that they are in the correct order
self.sections = grouped.map { day -> Day in
Day(title: day.key, occurrences: day.value, date: day.value[0].start)
}.sorted { $0.date < $1.date }
}
/// This is a helper function to quickly create dates so that this code will work. You probably don't need this in your code.
static func constructDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0) -> Date {
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.timeZone = TimeZone(abbreviation: "GMT")
dateComponents.hour = hour
dateComponents.minute = minute
// Create date from components
let userCalendar = Calendar.current // user calendar
let someDateTime = userCalendar.date(from: dateComponents)
return someDateTime!
}
}
This then allows the ContentView to simply be two nested ForEach.
struct ContentView: View {
// this mocks your data
let events = EventData()
var body: some View {
NavigationView {
List {
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.occurrences) { occurrence in
NavigationLink(destination: OccurrenceDetail(occurrence: occurrence)) {
OccurrenceRow(occurrence: occurrence)
}
}
}
}
}.navigationBarTitle(Text("Events"))
}
}
}
// These are sample views so that the code will work
struct OccurrenceDetail: View {
let occurrence: Occurrence
var body: some View {
Text(occurrence.title)
}
}
struct OccurrenceRow: View {
let occurrence: Occurrence
var body: some View {
Text(occurrence.title)
}
}
This is the end result.
This is actually two questions.
In the data part, please upgrade userData.occurrences from [Occurrence] to [[
Occurrence
]] (I called it latestOccurrences, here)
var self.userData.latestOccurrences = Dictionary(grouping: userData.occurrences) { (occurrence: Occurrence) -> String in
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter.string(from: occurrence.start)
}.values
In the swiftUI, you just reorganize the latest data:
NavigationView {
List {
ForEach(userData.latestOccurrences, id:\.self) { occurrenceSameDate in
Section(header: Text("\(occurrenceSameDate[0].start, formatter: DateFormatter.init())")) {
ForEach(occurrenceSameDate){ occurrence in
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
}
}
}
.navigationBarTitle(Text("Events"))
}.onAppear(perform: populate)

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

Resources