Using three arrays to populate complication timeline - swift

I have three arrays that have the data to populate the complication timeline with entries.
When I scroll through time travel, the complication does not change so I know I must be doing something wrong.
func getTimelineEntriesForComplication(complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
for headerObject in headerArray! {
for body1Object in body1Array! {
for body2Object in body2Array! {
let headerTextProvider = CLKSimpleTextProvider(text: headerObject as! String)
let body1TextProvider = CLKSimpleTextProvider(text: body1Object as! String)
let body2TextProvider = CLKRelativeDateTextProvider(date: body2Object as! NSDate, style: .Offset, units: .Day)
print("HeaderTextProvider: \(headerTextProvider)")
print("Body1TextProvider: \(body1TextProvider)")
print("Body2TextProvider: \(body2TextProvider)")
let template = CLKComplicationTemplateModularLargeStandardBody()
template.headerTextProvider = headerTextProvider
template.body1TextProvider = body1TextProvider
template.body2TextProvider = body2TextProvider
let timelineEntry = CLKComplicationTimelineEntry(date: body2Object as! NSDate, complicationTemplate: template)
entries.append(timelineEntry)
print("TimeEnt: \(entries)")
print("TimeEntCount: \(entries.count)")
}
}
}
handler(entries)
}
My thinking:
Loop through the three arrays
Set the template with the results of the array loops
Set the timeline entry with the date of the object in body2Array
The output on my console is:
HeaderTextProvider: <CLKSimpleTextProvider: 0x78e3f800>
Body1TextProvider: <CLKSimpleTextProvider: 0x78e4eb30>
Body2TextProvider: <CLKRelativeDateTextProvider: 0x78e4f050>
TimeEnt: [<CLKComplicationTimelineEntry: 0x78e4edd0> date = 2016-03-21 05:00:00 +0000, template = <CLKComplicationTemplateModularLargeStandardBody: 0x78e4edf0>, animationGroup = (null), <CLKComplicationTimelineEntry: 0x78e4f520> date = 2016-10-01 17:00:00 +0000, template = <CLKComplicationTemplateModularLargeStandardBody: 0x78e4f540>, animationGroup = (null)]
TimeEntCount: 2

Why time travel isn't working the way you expect:
Time travel only supports a 48-hour sliding window. Any timeline entries outside the complication server's latestTimeTravelDate will be ignored.
When constructing your timeline, do not create any entries after this date. Doing so is a waste of time because those entries will not be displayed right away.
You can't time travel over six months ahead to October 1, so your Mar 21 entry would never change to show the October 1 entry.
Other issues:
You probably don't mean to iterate through every body object for each header object.
You also want to start with an empty entries array within this method, so you're not inadvertently appending to an array with any existing (backwards) timeline entries.
Change your loop to look something like this:
// Unwrap optional arrays. You can also check counts if there's the possibility that they are not equally sized.
guard let headers = headerArray, texts = body1Array, dates = body2Array else { return handler(nil) }
var entries = [CLKComplicationTimelineEntry]()
for (index, header) in headers.enumerate() {
let text = texts[index]
let date = dates[index]
...
}
print("TimeEntCount: \(entries.count)")
This will give you headerArray.count timeline entries, instead of headerArray.count x body1Array.count x body2Array.count entries.
You may also want to specify the type of objects in each array so you don't have to constantly use as!. This will provide type safety and let the compiler type-check your code.
It also might make your code more readable and maintainable if you keep the data in an array of structs (with header, text, and date properties), instead of using multiple arrays.

Related

How do I write a swift function that will count the number of days without an entry?

I am working on a project with NSManagedObjects where each object is a user entry that has a value and a date. The app will be running a 7 day average and a 14 day average of the values the user enters. I have already set it up to do the appropriate fetch requests, sum the values, and divide by 7 and 14, respectively. However, I am realizing that when the user first begins using the app these running average values will be very misleading, so I would like to set up a function that will evaluate the number of days out of the last 7 and 14 that do not have any entries so I can subtract that value from the denominator in these calculations. I am a relative beginner and am having a hard time getting my head around how to write this function though, so any help would be appreciated. Thanks in advance.
Edit in response to Drekka:
The code I'm working from is below. I'm sorry for the broad question but I can't quite figure out where to start with structuring a looping function for what I'm trying to do and I haven't been able to come up with any examples or analogues in the searching I've done. Basically I'm pulling all of the values entered in the last seven days but I'm trying to figure out a way to evaluate for days where no values were entered within the span of this fetch request.
func sevenDayFetch() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Entry")
var calendar = Calendar.current
calendar.timeZone = NSTimeZone.local
let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: Date())
let dateFrom = calendar.startOfDay(for: sevenDaysAgo!)
let dateTo = Date()
let fromPredicate = NSPredicate(format: "entryDate > %#", dateFrom as NSDate)
let toPredicate = NSPredicate(format: "entryDate <= %#", dateTo as NSDate)
let sevenDayPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate])
fetchRequest.predicate = sevenDayPredicate
do {
entryArray = try managedContext.fetch(fetchRequest)
var sevenDayArray: [Int] = []
for i in entryArray as [NSManagedObject] {
sevenDayArray.append(i.value(forKey: "Value") as! Int)
}
let sevenDaySum = sevenDayArray.reduce(0, +)
let sevenDayAverage = sevenDaySum/7
sevenDayAverageLabel.text = String(sevenDayAverage)
I find it helpful to save the install date of the app in UserDefaults.
func installDate() -> NSDate {
var installDate: Date
if let date = UserDefaults.standard.object(forKey: UserDefaultsKeys.dateInstalled) as? Date {
installDate = date
} else {
installDate = Date()
UserDefaults.standard.set(installDate, forKey: UserDefaultsKeys.dateInstalled)
}
return installDate
}
Core data is not well suited for storing a single global value. And you cannot infer the install date from core-data; not having a value for a date does not mean that the was not install then.
Once you know the install date you can adjust calculations and your UI, if it is less than 14 or 7 days.

Swift: check if date is the same date as any day in array of dates?

I have an array of date objects - posts. And I am looping through a month. For each day, I want to check if some date in the array is on the same day. So far I have this:
var date = month?.startOfMonth()
var end = month?.endOfMonth()
while date! <= end! {
if posts.reduce(false,{Calendar.current.isDate(date, inSameDayAsDate: post.timeStamp)}) == true {
....
}
date = Calendar.current.date(byAdding: .day, value: 1, to: date!)
}
I believe that this starts with false and for each day in posts it checks whether its in the same day and if it is it turns the result into true. However I think it also changes it back to false the next time it encounters a false value...
What I want is something that returns true if any of the dates in posts is the same as some day rather than the last one. How can I do this?
Your current code is mostly OK though I would replace reduce with contains.
if let start = month?.startOfMonth(), let end = month?.endOfMonth() {
var date = start
var found = false
while !found && date <= end {
if posts.contains { Calendar.current.isDate(date, inSameDayAs: $0.timeStamp) } {
found = true
}
date = Calendar.current.date(byAdding: .day, value: 1, to: date)
}
if found {
// We have a match
}
}
I am basically building a stack view - for every day - I create a rectangle which is blue if there is a post that day and clear if not. Thus I probably do need to know the day. However filtering the array for elements which are in the specified month seems interesting. Can you show how to do that? Perhaps I could specify the location of just those days and then fill the rest of the stackArray with clear values using insertItem atIndex
Basically, I might start with two functions, one to filter the dates by month and one to filter by day. The reason I would so this, in your case, is you for each day, you don't want to refilter all the available dates for the month (but that's just me)
func dates(_ dates: [Date], withinMonth month: Int) -> [Date] {
let calendar = Calendar.current
let components: Set<Calendar.Component> = [.month]
let filtered = dates.filter { (date) -> Bool in
calendar.dateComponents(components, from: date).month == month
}
return filtered
}
func dates(_ dates: [Date], forDay day: Int) -> [Date] {
let calendar = Calendar.current
let components: Set<Calendar.Component> = [.day]
let filtered = dates.filter { (date) -> Bool in
calendar.dateComponents(components, from: date).day == day
}
return filtered
}
You could, use a contains approach, matching both the month and day, but again, there is an overhead to consider. In the above example, you could simply check to see if the day is contained in the resulting filtered dates by month, which might be closer to you desired result
nb This is not as efficient as something like first or contains as this will iterate the entire array finding every matching element, but, it has the nice side effect of providing you with more information. For example, you could sort the resulting filters and simply iterate from the start of the month to the end, popping off each match day as it occurs, as an idea
Thinking out loud...
Another approach might be to filter the available date's by the month, as above, but then to map the result to a Set of days (ie Int), this would allow you to either iterate over each day of the month and use contains(day) to perform a simple check to see if the day is contained or not.
Equally, you could map the view's to each day and iterate of the Set, changing the state of each view.
This all depends on more context then is available, but needless to say, there are any number of ways you might approach this problem
[Updated] As rightly mentioned already, you might be more interested in having a Set of days that have at least one post, something like:
let dayComponents: Set<Calendar.Component> = [.day, .month, .year, .era]
let calendar = Calendar.current
let daysWithPosts = Set(posts.map { post in
calendar.dateComponents(dayComponents, from: post.date)
})
Then for each date you can check if it's in that set (context unchanged, mind the force unwraps):
while date! <= end! {
let currentDayComponents = calendar.dateComponents(dayComponents, from: date)
let postsFound = daysWithPosts.contains(currentDayComponents)
// <use postsFound as needed>
date = Calendar.current.date(byAdding: .day, value: 1, to: date!)
}
Original answer, adapted for multiple dates:
This should tell if there are posts on a given date's day:
func areTherePosts(in posts: [Post], fromSameDayAs date: Date) -> Bool {
let calendar = Calendar.current
let dayComponents: Set<Calendar.Component> = [.day, .month, .year, .era]
let specificDateComponents = calendar.dateComponents(dayComponents, from: date)
return posts.contains { post in
calendar.dateComponents(dayComponents, from: post.date) == specificDateComponents
}
}
Usage in your context (again, unchanged):
while date! <= end! {
let postsFound = areTherePosts(in: posts, fromSameDayAs: date!)
// <use postsFound as needed>
date = Calendar.current.date(byAdding: .day, value: 1, to: date!)
}

pulling not consistent data in firebase

I am working on a firebase project and decided to make some changes in the data structure. Now, I decided it would be better to add a placeholder image to the node below (Before, I used a dummy image to serve as a placeholder). Is there a way I can still get the dates below? and add the placeholder URL to another structure?
So what I would want is an array dates = ["20180203","20180204","20180205"] and then another array containing placeholders = ["https.googleapis.whateverIsInTheDay80180203", "https.dummydefaultimage","https.dummydefaultimage"]
public func getAvailableDates(spotTitle:String, handler: #escaping (_ dateList:[String])->())
{
var datesList:[String] = []
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd"
let allowedDays = 30
ref.child("ImageLocationDates").child(spotTitle).observe(DataEventType.value, with: { (snapshot) in
if snapshot.childrenCount > 0 {
for mydata in snapshot.children.allObjects as! [DataSnapshot]
{
let date = mydata.key
if date.count == 8 {
let testDate = formatter.date(from: date)
let cal = Calendar.current
let components = cal.dateComponents([.day], from: testDate!, to: cal.startOfDay(for: Date()))
let dateDistance = components.day! // integer of distance between today and date provided
if dateDistance < allowedDays {
if !datesList.contains(convertDate(stringDate: date)){
datesList.insert(convertDate(stringDate: date), at: 0)
}
else {
print("getting duplicate data")
}
}
}
}
handler(datesList)
}
})
}
In Firebase real time database you can have a few ways to structure you data. Keep in mind you want your data not to get too deep. Usually my limit is 3 folds not more. To keep things shallow then, you can normalize and distribute your data which includes a bit of duplication sometimes but it can speed up queries and will decrease the amount of data users download each time.
Look at three structures below :
(A)
D_street:
2012491203 :
placeholder: "https://...."
2012491203 :
placeholder: "https://...."
...
(B)
D_street_dates:
2012491203 : true
2012491203 :
...
D_street_placeholder:
2012491203 : "https://...."
2012491203 : "https://...."
...
(C)
D_street_dates:
id1 :
date: 2012491203
placeholder: "https://...."
id2 :
date: 2012491203
placeholder: "https://...."
...
Each one of structures above can be great depending on your use case. in A you can query by the date (key) and once you get the results you have actually downloaded the placeholders too because they are the values.
In B you only download date when you query D_street_dates and then you have to do another query to get the placeholder per each date from D_street_placeholder.
In C you structure data per an ID or userID or something that both date and placeholders are properties of and whenever you query ids you get both dates and placeholder for that.

Accessing Firebase Data inside unique AutoID

This is my first question and I'm still learning Swift/Xcode/Firebase, so I appreciate your patience. I've been stalking StackOverflow and have found a lot of answers to help with various things, but nothing that makes sense for the problem I've been struggling with for 2 days.
I am writing a program that will save a date picked on a previous viewcontroller and a set of user-entered floats from text fields to a Firebase database, and append each data set as a separate entry instead of overwriting the previous data. Using the first block of code below, I've got this problem solved except I can't find a way to do it without using AutoID. This leaves me with a setup like this in Firebase, but with multiple categories and "optionSelected" sections in each category:
program-name
Category 1
optionSelected
L1cggMnqFqaJf1a7UOv
Date: "21-12-2017"
Variable 1 Float: "12345"
Variable 2 Float: "26.51"
L1ciVpLq1yXm5khimQC
Date: "30-12-2017"
Variable 1 Float: "23456"
Variable 2 Float: "35.88"
Code used to save:
func newWithNewVars() {
let myDatabase = Database.database().reference().child("Category 1").child(optionSelected)
let variable1 = textField1.text
let variable2 = textField2.text
let variable1Float = (textField1.text! as NSString).floatValue
let variable2Float = (textField2.text! as NSString).floatValue
let writeArray = ["Date": textPassedOverDate, "Variable 1 Float": variable1Float, "Variable 2 Float": variable2Float]
myDatabase.childByAutoId().setValue(gasArray) {
(error, reference) in
if error != nil {
print(error!)
}
else {
print("Message saved successfully!")
}
}
}
The problem comes with recalling data. Since the AutoID is unique, I can't figure out how to access the data deeper inside for calculations. Specifically, I want to be able to make a new entry, press the save data button, and have it find the most recent entry in the "optionSelected" section so it can do calculations like subtract the older variable 1 from the new variable 1 and such.
Given the above description, layout, and code used above, what code structure would allow me to find the most recent date and access the data inside the AutoID sections for a specific category and "optionSelected"?
Thank you for your help.
The issue you're having is that you're trying to dig deeper but can't as you don't have a hold of that id. You'll want to use the .childAdded in your reference observation when you want to get inside of a list in your JSON tree when you don't have a hold of that id to get inside - this will be called as many times as there are values inside of Category 1 tree:
let reference = Database.database().reference()
reference.child("Category 1").child("optionSelected").observe(.childAdded, with: { (snapshot) in
let uniqueKey = snapshot.key // IF YOU WANT ACCESS TO THAT UNIQUE ID
print(uniqueKey)
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
let date = dictionary["date"] as? String
let variableOne = dictionary["Variable 1 Float"] as? Float
let variableOne = dictionary["Variable 2 Float"] as? Float
}, withCancel: nil)
You may also want to avoid using spaces in your database keys to avoid any problems in the near future. I'd stick with the common lowercased underscore practice e.g. "category_1" or "variable_2_float"

Use Measurement for input in NSTextFieldCell

I want to be able to nicely use a Measurement and MeasurementFormatter for output and input with a NSTextFieldCell.
I am able to display the measurement correctly with...
let areaFormatter = MeasurementFormatter()
areaFormatter.unitStyle = .medium
areaFormatter.unitOptions = .providedUnit
let area = Measurement<UnitArea>( value: 123.43, unit: .squareInches)
let editInput = NSTextFieldCell
editInput.objectValue = area
editInput.formatter = areaFormatter
This displays something like
123.43 in^2
The problem starts when I want to read this back in with
var inputArea = editInput.objectValue as! Measurement<UnitArea>
I think because the get Object value of the Measurement Formatter is not defined.
open func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool
Is my understanding correct? Are there any examples in Swift where this function has been defined for measurements?
Right now the user can edit the entire string including the text in the units. Is there a good way to block the units in the NSTextFieldCell? I would like the user to be able to edit the number but not the units and then return the measurement with
var inputArea = editInput.objectValue as! Measurement<UnitArea>
so this gets displayed
123.43 in^2
but only the 123.43 can be edited.