iOS Swift convenience initializer self used before self.init called - ios

so I have been trying to create a "Period" class, with the following attributes:
class PTPeriod {
// MARK: Stored Properties
var start: Date
var end: Date
var place: PTPlace?
//MARK: DESIGNATED NITIALIZER
init(start: Date, end: Date, place: PTPlace? = nil) {
self.start = start
self.end = end
self.place = place
}
And I want to initialize it with a convenience init that accepts a dictionary like the following:
{
"close" : {
"day" : 1,
"time" : "0000"
},
"open" : {
"day" : 0,
"time" : "0900"
}
}
This is my initializer, originally I put the heavy lifting into a helper method. However, i was getting the same error I have now so I removed the helper method and the error is still occurring. Unsure where it thinks I am calling self.
UPDATE: moved the date formatting into a date formatter extension, but the error still persists! unsure WHY. new init shown:
My new Convenience init:
// MARK: Convenience init for initializing periods pulled from Google Place API
convenience init?(placeData: [String:Any], place: PTPlace? = nil) throws {
var start: Date? = nil
var end: Date? = nil
var formatter = DateFormatter()
do {
for (key, value) in placeData {
let period = value as! [String: Any]
if (key == "open") {
start = try formatter.upcoming_date(with: period)
} else if (key == "close") {
end = try formatter.upcoming_date(with: period)
} else {
break
}
}
if (start != nil && end != nil) { self.init(start: start!, end: end!, place: place) }
else { print("we f'd up") }
}
catch { print(error.localizedDescription) }
}
Here is my DateFormatter.upcoming_date method:
func upcoming_date(with googlePlacePeriod: [String: Any]) throws -> Date? {
if let day = googlePlacePeriod["day"] as? Int {
if (day < 0) || (day > 6) { throw SerializationError.invalid("day", day) } // throw an error if day is not between 0-6
if let time = googlePlacePeriod["time"] as? String {
if time.characters.count != 4 { throw SerializationError.invalid("time", time) } // throw an error if time is not 4 char long
var upcoming_date: Date
let gregorian = Calendar(identifier: .gregorian)
let current_day_of_week = Date().getDayOfWeekInt()
let dayOfPeriod = day + 1
///TRUST THAT THIS WORKS... NO JODAS
var distance = dayOfPeriod - current_day_of_week // inverse = false
if (dayOfPeriod < current_day_of_week) {
switch distance {
case -1: distance = 6
case -2: distance = 5
case -3: distance = 4
case -4: distance = 3
case -5: distance = 2
case -6: distance = 1
default: break
}
}
/** time example: "1535" translates into 3:35PM
first two characters in string are "hour" (15)
last two characters in string are "minute(35)
**/
let hour = Int(time.substring(to: time.index(time.startIndex, offsetBy: 2)))!
let minute = Int(time.substring(from: time.index(time.endIndex, offsetBy: -2)))
upcoming_date = Date().fastForward(amount: distance, unit: "days", inverse: false)!
var components = gregorian.dateComponents([.year, .month, .day, .hour, .minute, .second], from: upcoming_date)
components.hour = hour
components.minute = minute
upcoming_date = gregorian.date(from: components)!
return upcoming_date
}
else { throw SerializationError.missing("time") }
}
else { throw SerializationError.missing("day") }
}

You have a failable initializer, so if it fails, you must return nil to let it know it failed:
if (start != nil && end != nil) {
self.init(start: start!, end: end!, place: place) }
} else {
print("we f'd up")
return nil
}
Also, in your do-catch, you need to either
re-throw the error in the catch block
catch {
print(error.localizedDescription)
throw error
}
eliminate the do-catch blocks altogether; or
just return nil:
catch {
print(error.localizedDescription)
return nil
}
If you do this, you'd presumably not define this failable initializer as one that throws because you're no longer throwing.
Given that you're unlikely to do anything meaningful with the thrown error, I'd just make it a failable initializer that doesn't throw:
convenience init?(placeData: [String: Any], place: PTPlace? = nil) {
var start: Date?
var end: Date?
let formatter = DateFormatter()
for (key, value) in placeData {
let period = value as! [String: Any]
if key == "open" {
start = try? formatter.upcoming_date(with: period)
} else if key == "close" {
end = try? formatter.upcoming_date(with: period)
} else {
break // I'm not sure why you're doing this; it seems extremely imprudent (esp since dictionaries are not ordered)
}
}
if let start = start, let end = end {
self.init(start: start, end: end, place: place)
} else {
print("either start or end were not found")
return nil
}
}

Related

Check if current time is between range of times considering after midnight - Swift

I have an opening and closing hours of restaurants from rest service:
[
{
"weekday":0,
"openingAt":"07:30:00",
"closingAt":"00:45:00"
},
{
"weekday":1,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":2,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":3,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":4,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":5,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":6,
"openingAt":"07:30:00",
"closingAt":"01:00:00"
}
]
I have created computed property for a check that:
var isClosed: Bool {
let todayIndex = (Calendar.current.component(.weekday, from: Date()) - 1) % 7
let yesterdayIndex = (Calendar.current.component(.weekday, from: Date()) - 2) % 7
let todayDate = Date()
if let wh = workingHour, wh.count > 7 {
let todayWh = wh[todayIndex]
if let openingStr = todayWh.openingAt, openingStr != "",
let openingDate = Date().setTimeHHmmss(formattedString: openingStr),
let closingStr = todayWh.closingAt, closingStr != "",
let closingDate = Date().setTimeHHmmss(formattedString: closingStr)
{
let yesterdayWh = wh[yesterdayIndex]
var fromYesterdayExtraHours = 0
if let yesterdayClosingStr = yesterdayWh.closingAt,
let yClosingDate = Date().setTimeHHmmss(formattedString: yesterdayClosingStr) {
if yClosingDate.hour > 0 {
fromYesterdayExtraHours = yClosingDate.hour
}
}
if closingDate < openingDate {
if todayDate.hour < fromYesterdayExtraHours {
return false // opened
} else {
return true // closed
}
} else if todayDate >= openingDate && todayDate <= closingDate {
return false // opened
} else {
return true // closed
}
}
}
return false // opened
}
What I do is:
Converting the strings to date object by setting the time on today object
Then checking if the closing time is on the next day (the worst part)
Then checking if the closing time is less than opening time, (like this: "openingAt":"07:30:00", "closingAt":"01:00:00"
Then checking if the current time is between opening and closing time
Any Swifty way suggestion to fix this mess?
Here is my solution that takes a little different approach by using a struct with minute and hour as Int's
struct Time: Comparable {
var hour = 0
var minute = 0
init(hour: Int, minute: Int) {
self.hour = hour
self.minute = minute
}
init(_ date: Date) {
let calendar = Calendar.current
hour = calendar.component(.hour, from: date)
minute = calendar.component(.minute, from: date)
}
static func == (lhs: Time, rhs: Time) -> Bool {
return lhs.hour == rhs.hour && lhs.minute == rhs.minute
}
static func < (lhs: Time, rhs: Time) -> Bool {
return (lhs.hour < rhs.hour) || (lhs.hour == rhs.hour && lhs.minute < rhs.minute)
}
static func create(time: String) -> Time? {
let parts = time.split(separator: ":")
if let hour = Int(parts[0]), let minute = Int(parts[1]) {
return Time(hour: hour, minute: minute)
}
return nil
}
static func isOpen(open: Time, close: Time) -> Bool {
let isClosingAfterMidnight = close.hour < open.hour ? true : false
let currentTime = Time(Date())
if isClosingAfterMidnight {
return currentTime > close && currentTime < open ? false : true
}
return currentTime >= open && currentTime < close
}
}
And it can be used like
if let open = Time.create(time: todayWh.openingAt), let close = Time.create(time: todayWh.closingAt) {
return Time.isOpen(open: open, close: close))
} else {
//error handling
}
It should work also after midnight :) Of course the Time struct could be used directly in the wh array.
My suggestion is to decode the JSON into a struct with Decodable and the opening and closing times for convenience reasons into DateComponents.
I assume that weekday == 0 is Sunday
let jsonString = """
[
{"weekday":0, "openingAt":"07:30:00", "closingAt":"00:45:00"},
{"weekday":1, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":2, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":3, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":4, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":5, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":6, "openingAt":"07:30:00", "closingAt":"01:00:00"}
]
"""
struct Schedule : Decodable {
let weekday : Int
let openingAt : DateComponents
let closingAt : DateComponents
private enum CodingKeys: String, CodingKey { case weekday, openingAt, closingAt }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
weekday = (try container.decode(Int.self, forKey: .weekday)) + 1
let open = try container.decode(String.self, forKey: .openingAt)
let openComponents = open.components(separatedBy:":")
openingAt = DateComponents(hour: Int(openComponents[0]), minute: Int(openComponents[1]), second: Int(openComponents[2]))
let close = try container.decode(String.self, forKey: .closingAt)
let closeComponents = close.components(separatedBy:":")
closingAt = DateComponents(hour: Int(closeComponents[0]), minute: Int(closeComponents[1]), second: Int(closeComponents[2]))
}
}
First declare now, start of today and the current calendar
let now = Date()
let calendar = Calendar.current
let midnight = calendar.startOfDay(for: now)
Then decode the JSON, filter the schedule for today by its weekday, create opening and closing time by adding the date components and compare the dates. endDate is calculated by finding the next occurrence of given date components after the start date. This solves the issue if the closing date is tomorrow.
do {
let data = Data(jsonString.utf8)
let schedule = try JSONDecoder().decode([Schedule].self, from: data)
let todaySchedule = schedule.first{ $0.weekday == calendar.component(.weekday, from: now) }
let startDate = calendar.date(byAdding: todaySchedule!.openingAt, to: midnight)!
let endDate = calendar.nextDate(after: startDate, matching: todaySchedule!.closingAt, matchingPolicy: .nextTime)!
let isOpen = now >= startDate && now < endDate
} catch { print(error) }
There is one limitation: The code does not work if the current time is in the range 0:00 to closing time (for example Sunday and Monday). That's a challenge for you πŸ˜‰

Countdown timer in table view cell shows different values after scrolling

The problem is described in title, but to be more specific here is a full picture.
I have a custom table view cell subclass with label inside it displaying the countdown timer. When there a small portion of timers it works fine, but with a lot of data I need to display timers far beyond the visible cells and when I scroll down fast and then scroll up fast, the timer values in cells start to show different values until a certain point in time, after which it shows the right value.
I tried different variants for those reuseable cells, but I can’t spot a problem. Help needed!!!
Here is the code of implementation of logic.
Custom cell subclass:
let calendar = Calendar.current
var timer: Timer?
var deadlineDate: Date? {
didSet {
updateTimeLabel()
}
}
override func awakeFromNib() {
purchaseCellCardView.layer.cornerRadius = 10
let selectedView = UIView(frame: CGRect.zero)
selectedView.backgroundColor = UIColor.clear
selectedBackgroundView = selectedView
}
override func prepareForReuse() {
super.prepareForReuse()
if timer != nil {
print("Invalidated!")
timer?.invalidate()
timer = nil
}
}
func configure(for purchase: Purchase) {
purchaseSubjectLabel.text = purchase.subject
startingPriceLabel.text = purchase.NMC
stageLabel.text = purchase.stage
fzImageView.image = purchase.fedLaw.contains("44") ? #imageLiteral(resourceName: "FZ44") : #imageLiteral(resourceName: "FZ223")
timeLabel.isHidden = purchase.stage == "Π Π°Π±ΠΎΡ‚Π° комиссии"
warningImageView.image = purchase.warningImage
}
func updateTimeLabel() {
setTimeLeft()
timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in
guard let strongSelf = self else {return}
strongSelf.setTimeLeft()
}
RunLoop.current.add(timer!, forMode: .commonModes)
}
#objc private func setTimeLeft() {
let currentDate = getCurrentLocalDate()
if deadlineDate?.compare(currentDate) == .orderedDescending {
var components = calendar.dateComponents([.day, .hour, .minute, .second], from: currentDate, to: deadlineDate!)
let dayText = (components.day! == 0 || components.day! < 0) ? "" : String(format: "%i", components.day!)
let hourText = (components.hour == 0 || components.hour! < 0) ? "" : String(format: "%i", components.hour!)
switch (dayText, hourText) {
case ("", ""):
timeLabel.text = String(format: "%02i", components.minute!) + ":" + String(format: "%02i", components.second!)
case ("", _):
timeLabel.text = hourText + " Ρ‡."
default:
timeLabel.text = dayText + " Π΄Π½."
}
} else {
stageLabel.text = "Π Π°Π±ΠΎΡ‚Π° комиссии"
timeLabel.text = ""
timeLabel.isHidden = true
timer?.invalidate()
}
}
private func getCurrentLocalDate() -> Date {
var now = Date()
var nowComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now)
nowComponents.timeZone = TimeZone(abbreviation: "UTC")
now = calendar.date(from: nowComponents)!
return now
}
deinit {
print("DESTROYED")
timer?.invalidate()
timer = nil
}
The most important part of tableView(_cellForRowAt:)
case .results:
if filteredArrayOfPurchases.isEmpty {
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCellIdentifiers.nothingFoundCell,
for: indexPath)
let label = cell.viewWithTag(110) as! UILabel
switch segmentedControl.index {
case 1:
label.text = "НСт Π·Π°ΠΊΡƒΠΏΠΎΠΊ способом\n«Запрос ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠΉΒ»"
case 2:
label.text = "НСт Π·Π°ΠΊΡƒΠΏΠΎΠΊ способом\nΒ«ΠšΠΎΠ½ΠΊΡƒΡ€ΡΒ»"
case 3:
label.text = "НСт Π·Π°ΠΊΡƒΠΏΠΎΠΊ способом\n«Аукцион»"
default:
label.text = "НСт Π·Π°ΠΊΡƒΠΏΠΎΠΊ способом\n«Запрос ΠΊΠΎΡ‚ΠΈΡ€ΠΎΠ²ΠΎΠΊΒ»"
}
return cell
} else {
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCellIdentifiers.purchaseCell,
for: indexPath) as! PurchaseCell
cell.containerViewTopConstraint.constant = indexPath.row == 0 ? 8.0 : 4.0
cell.containerViewBottomConstraint.constant = indexPath.row == filteredArrayOfPurchases.count - 1 ? 8.0 : 4.0
let purchase = filteredArrayOfPurchases[indexPath.row]
cell.configure(for: purchase)
if cell.timer != nil {
cell.updateTimeLabel()
} else {
search.getDeadlineDateAndTimeToApply(purchase.purchaseURL, purchase.fedLaw, purchase.stage, completion: { (date) in
cell.deadlineDate = date
})
}
return cell
}
And the last piece of a puzzle:
func getDeadlineDateAndTimeToApply(_ url: URL?, _ fedLaw: String, _ stage: String, completion: #escaping (Date) -> ()) {
var deadlineDateAndTimeToApply = Date()
guard stage != "Π Π°Π±ΠΎΡ‚Π° комиссии" else { return }
if let url = url {
dataTask = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error as NSError?, error.code == -403 {
// TODO: Add alert here
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data, let html = String(data: data, encoding: .utf8), let purchasePageBody = try? SwiftSoup.parse(html), let purchaseCard = try? purchasePageBody.select("td").array() else {return}
let mappedArray = purchaseCard.map(){String(describing: $0)}
if fedLaw.contains("44") {
guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td class=\"fontBoldTextTd\">Π”Π°Ρ‚Π° ΠΈ врСмя окончания ΠΏΠΎΠ΄Π°Ρ‡ΠΈ заявок</td>"))! + 1].text().components(separatedBy: " ") else {return}
dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
let deadlineDateToApply = deadlineDateToApplyString.first!
let deadlineTimeToApply = deadlineDateToApplyString[1]
guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else {return}
deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
} else {
guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td>Π”Π°Ρ‚Π° ΠΈ врСмя окончания ΠΏΠΎΠ΄Π°Ρ‡ΠΈ заявок<br> (ΠΏΠΎ мСстному Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π·Π°ΠΊΠ°Π·Ρ‡ΠΈΠΊΠ°)</td>"))! + 1].text().components(separatedBy: " ") else {return}
dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
let deadlineDateToApply = deadlineDateToApplyString.first!
let deadlineTimeToApply = deadlineDateToApplyString[2]
guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else {return}
deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
}
DispatchQueue.main.async {
completion(deadlineDateAndTimeToApply)
}
})
dataTask?.resume()
}
}
A few notes:
Tried resetting deadlineDate to nil in prepareForReuse() - doesn’t help;
Using SwiftSoup Framework to parse HTML as you can see in the last code example if it matters.
This is quite a lot of code but from what you are describing your issue is in reusing cells.
You would do well to separate the timers out of the cells and put them inside your objects. It is where they belong (or in some manager like view controller). Imagine having something like the following:
class MyObject {
var timeLeft: TimeInterval = 0.0 {
didSet {
if timeLeft > 0.0 && timer == nil {
timer = Timer.scheduled...
} else if timeLeft <= 0.0, let timer = timer {
timer.invalidate()
self.timer = nil
}
delegate?.myObject(self, updatedTimeLeft: timeLeft)
}
}
weak var delegate: MyObjectDelegate?
private var timer: Timer?
}
Now all you need is is a cell for row at index path to assign your object: cell.myObject = myObjects[indexPath.row].
And your cell would do something like:
var myObject: MyObject? {
didSet {
if oldValue.delegate == self {
oldValue.delegate = nil // detach from previous item
}
myObject.delegate = self
refreshUI()
}
}
func myObject(_ sender: MyObject, updatedTimeLeft timeLeft: TimeInterval) {
refreshUI()
}
I believe the rest should be pretty much straight forward...
Your problem is here:
search.getDeadlineDateAndTimeToApply(purchase.purchaseURL,
purchase.fedLaw,
purchase.stage,
completion: { (date) in
cell.deadlineDate = date
})
getDeadlineDateAndTimeToApply runs asynchronously, calculates something, and then updates the cell.deadlineData in the main thread (which is fine). But in the meantime, while calculating something, the user might have scrolled up and down, the cell might have been reused for another row, and now the update updates the cell incorrectly.
What you need to do is: Do not store the UITableViewCell directly. Instead, keep track of the IndexPath to be updated, and once the caluclation is done, retrieve the the cell that belongs to that IndexPath and update this.

Fetch "transcript" values from Google speech api

I am trying to fetch the "transcript" value from the following result:
{
transcript: "1 2 3 4"
confidence: 0.902119
words {
start_time {
nanos: 200000000
}
end_time {
nanos: 700000000
}
word: "1"
}
words {
start_time {
nanos: 700000000
}
end_time {
nanos: 900000000
}
word: "2"
}
words {
start_time {
nanos: 900000000
}
end_time {
seconds: 1
}
word: "3"
}
words {
start_time {
seconds: 1
}
end_time {
seconds: 1
nanos: 300000000
}
word: "4"
}
}
The code I am writing to get it is :
for result in response.resultsArray! {
if let result = result as? StreamingRecognitionResult {
for alternative in result.alternativesArray {
if let alternative = alternative as? StreamingRecognitionResult {
textView.text = "\(alternative["transcript"])"
}
}
}
}
So when I am trying to put the value in textview.text I am getting an error stating :
"Type 'StreamingRecognitionResult' has no subscript members ".
Please help.
The key lines are:
let tmpBestResult = (response.resultsArray.firstObject as! StreamingRecognitionResult)
let tmpBestAlternativeOfResult = tmpBestResult.alternativesArray.firstObject as! SpeechRecognitionAlternative
let bestTranscript = tmpBestAlternativeOfResult.transcript
The placement of these lines inside the streamAudioData() is given below:
SpeechRecognitionService.sharedInstance.streamAudioData(audioData,languageCode: self.selectedLangType.rawValue,
completion:
{ [weak self] (response, error) in
guard let strongSelf = self else {
return
}
if let error = error {
debugPrint(">>)) Process_delegate error >> \(error.localizedDescription)")
strongSelf.stopRecordingSpeech()
self?.delegate.conversionDidFail(errorMsg: error.localizedDescription)
} else if let response = response {
var finished = false
debugPrint(response)
for result in response.resultsArray! {
if let result = result as? StreamingRecognitionResult {
if result.isFinal {
finished = true
}
}
}
let tmpBestResult = (response.resultsArray.firstObject as! StreamingRecognitionResult)
let tmpBestAlternativeOfResult = tmpBestResult.alternativesArray.firstObject as! SpeechRecognitionAlternative
let bestTranscript = tmpBestAlternativeOfResult.transcript
strongSelf.delegate.conversionOnProcess(intermediateTranscript: bestTranscript!, isFinal: finished)
if finished {
//UI
strongSelf.stopRecordingSpeech()
strongSelf.delegate.conversionDidFinish(finalTranscript: bestTranscript!)
}
}
})
Happy Coding ;)
So the code will be changed to this:
for alternative in result.alternativesArray {
if let alternative1 = alternative as? SpeechRecognitionAlternative {
textView.text = "\(alternative1.transcript)"
}
}

How to get HealthKit total steps for previous dates

I am trying to get steps from Health Kit, its working fine for mobile but when i connect Apple Watch my app get more steps then Health kit. i trace it and find that it collect detail record of steps but total steps are less then detail in Health kit.
My App getting the sum of these steps:
But I want To Get these:
Here is My Code:
func MultipleDaysStepsAndActivitiesTest(_ startDate:Date, completion: #escaping (NSDictionary, [HealthKitManuallActivity], NSError?) -> () ) {
let type = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount) // The type of data we are requesting
let now = Date()
let newDate = startDate
let predicate = HKQuery.predicateForSamples(withStart: newDate, end: now, options: HKQueryOptions())
var dates = now.datesBetweenGivenDates(startDate,endDate:now)
dates = dates.reversed()
let query = HKSampleQuery(sampleType: type!, predicate: predicate, limit: 0, sortDescriptors: nil) { query, results, error in
var dict:[String:Double] = [:]
if results?.count > 0 {
for result in results as! [HKQuantitySample] {
print(result)
if result.sourceRevision.source.name != kHealthKitSource {
if dict[self.fmt.string(from: result.startDate)] != nil {
dict[self.fmt.string(from: result.startDate)] = dict[self.fmt.string(from: result.startDate)]! + result.quantity.doubleValue(for: HKUnit.count())
} else {
dict[self.fmt.string(from: result.startDate)] = result.quantity.doubleValue(for: HKUnit.count())
}
}
}
}
var sDate = startDate // first date
let cal = Calendar.current
print(dict)
if dict.isEmpty {
while sDate <= Date() {
dict[self.fmt.string(from: sDate)] = 0
sDate = cal.date(byAdding: .day, value: 1, to: sDate)!
}
} else {
while sDate <= Date() {
if dict[self.fmt.string(from: sDate)] == nil {
dict[self.fmt.string(from: sDate)] = 0
}
sDate = cal.date(byAdding: .day, value: 1, to: sDate)!
}
}
// reading activities
self.MultipleDaysWorkouts(startDate, endDate: now, completion: { (activities, error) in
if results?.count == 0 {
for activity in activities {
dict[activity.startDate] = 0.0
}
}
// reading mindfulness activities
self.MultipleDayMindFullnessActivity(startDate, completion: { (mindfulnessActivities, mindError) in
if mindError == nil {
let allActivities = mindfulnessActivities + activities
completion(dict as NSDictionary, allActivities, mindError as NSError?)
}
})
})
}
execute(query)
}
You should use HKStatisticsQuery or HKStatisticsCollectionQuery instead of HKSampleQuery. The statistics queries will de-deuplicate overlapping step samples from different sources to ensure that you do not double-count them. You can find documentation for them here and here.

HKStatisticsCollectionQuery resultsHandler and NSOperation

In my project I am creating HKStatisticsCollectionQueries for a series of HKQuantityTypes. The resultsHandler then adds this data to an date-ordered array of objects. I want to do another operation only when the entire series of HKStatisticsCollectionQueries have been processed and the results appended to my array.
I have tried to do this by putting the task inside of a subclass of NSOperation, but the dependent block is fired before any of the samples are added to the array. According to the HKStatisticsCollectionQuery documentation "This method runs the query on an anonymous background queue. When the query is complete, it executes the results handler on the same background queue"
Is there a way to use HKStatisticsCollectionQuery's initialResultsHandler and statisticsUpdateHandler with NSOperation?
when I run this I get the following output:
cycleOperation start
cycleOperation CompletionBlock
dependentOperation start
dependentOperation CompletionBlock
SumStatistics addSamplesToArray: cycle: 96 samples added
SumStatistics main complete: 96 samples added
func getCycleKm(){
let sampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceCycling)
let hkUnit = HKUnit.meterUnitWithMetricPrefix(.Kilo)
println("cycleOperation start")
let cycleOperation = SumStatistics(quantityType: sampleType, startDate: startingDate, heathDataArray: self.healthDataArray)
cycleOperation.completionBlock = {println("cycleOperation CompletionBlock ")}
let dependentOperation = NSBlockOperation{()->Void in println("dependentOperation start")}
dependentOperation.completionBlock = {println("dependentOperation CompletionBlock")}
dependentOperation.addDependency(cycleOperation)
self.operationQueue.addOperation(cycleOperation)
self.operationQueue.addOperation(dependentOperation)
}
class SumStatistics:NSOperation{
let healthKitStore:HKHealthStore = HKHealthStore()
private let quantityType:HKQuantityType
private let startDate:NSDate
private let endDate: NSDate
private let statsOption: HKStatisticsOptions
var healthDataArray:[HealthData]
required init(quantityType:HKQuantityType, startDate:NSDate, heathDataArray:[HealthData]){
self.quantityType = quantityType
self.startDate = startDate
let startOfToday = NSDate().getStartOfDate()
self.endDate = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startOfToday, options: nil)!
self.statsOption = HKStatisticsOptions.CumulativeSum
self.healthDataArray = heathDataArray
super.init()
}
override func main() {
getSumStatistics { (hkSamples, statsError) -> Void in
self.addSamplesToArray(hkSamples)
println("SumStatistics main complete: \(hkSamples.count) samples added")
}
}
func addSamplesToArray(newSamples:[HKQuantitySample]){
var samples = newSamples
samples.sort({$0.startDate.timeIntervalSinceNow > $1.startDate.timeIntervalSinceNow})
if samples.count == 0{
println("SumStatistics addSamplesToArray: no samples!")
return
}
var ctr = 0
var typeString = ""
for healthDataDate in self.healthDataArray{
while healthDataDate.date.isSameDate(samples[ctr].startDate) && ctr < samples.count - 1{
switch samples[ctr].quantityType.identifier {
case HKQuantityTypeIdentifierBodyMass:
healthDataDate.weight = samples[ctr].quantity
typeString = "weight"
case HKQuantityTypeIdentifierDietaryEnergyConsumed:
healthDataDate.dietCalories = samples[ctr].quantity
typeString = "diet"
case HKQuantityTypeIdentifierActiveEnergyBurned:
healthDataDate.activeCalories = samples[ctr].quantity
typeString = "active"
case HKQuantityTypeIdentifierBasalEnergyBurned:
healthDataDate.basalCalories = samples[ctr].quantity
typeString = "basal"
case HKQuantityTypeIdentifierStepCount:
healthDataDate.steps = samples[ctr].quantity
typeString = "steps"
case HKQuantityTypeIdentifierDistanceCycling:
healthDataDate.cycleKM = samples[ctr].quantity
typeString = "cycle"
case HKQuantityTypeIdentifierDistanceWalkingRunning:
healthDataDate.runWalkKM = samples[ctr].quantity
typeString = "runWalk"
default:
println("SumStatistics addSamplesToArray type not found -> \(samples[ctr].quantityType)")
}
if ctr < samples.count - 1{
ctr += 1
}else{
break
}
}
}
println("SumStatistics addSamplesToArray: \(typeString): \(newSamples.count) samples added")
}
func getSumStatistics(completionHandler:([HKQuantitySample], NSError!)->Void){
let dayStart = NSCalendar.currentCalendar().startOfDayForDate(startDate)
let addDay = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: endDate, options:nil)
let dayEnd = NSCalendar.currentCalendar().startOfDayForDate(addDay!) //add one day
let interval = NSDateComponents()
interval.day = 1
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.None)
let newQuery = HKStatisticsCollectionQuery(quantityType: quantityType,
quantitySamplePredicate: predicate,
options: statsOption,
anchorDate: dayStart,
intervalComponents: interval)
newQuery.initialResultsHandler = {
query, statisticsCollection, error in
var resultsArray = [HKQuantitySample]()
if error != nil {
println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
}else{
statisticsCollection.enumerateStatisticsFromDate(self.startDate, toDate: self.endDate, withBlock: { (statistics, stop) -> Void in
if let statisticsQuantity = statistics.sumQuantity() {
let startD = NSCalendar.currentCalendar().startOfDayForDate(statistics.startDate)
let endD = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startD, options: nil)
let qSample = HKQuantitySample(type: self.quantityType, quantity: statisticsQuantity, startDate: startD, endDate: endD)
resultsArray.append(qSample)
}
})
}
completionHandler(resultsArray,error)
}
newQuery.statisticsUpdateHandler = {
query, statistics, statisticsCollection, error in
println("*** updateHandler fired")
var resultsArray = [HKQuantitySample]()
if error != nil {
println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
}else{
statisticsCollection.enumerateStatisticsFromDate(self.startDate, toDate: self.endDate, withBlock: { (statistics, stop) -> Void in
if let statisticsQuantity = statistics.sumQuantity() {
let startD = NSCalendar.currentCalendar().startOfDayForDate(statistics.startDate)
let endD = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startD, options: nil)
let qSample = HKQuantitySample(type: self.quantityType, quantity: statisticsQuantity, startDate: startD, endDate: endD)
resultsArray.append(qSample)
}
})
}
completionHandler(resultsArray,error)
}
self.healthKitStore.executeQuery(newQuery)
}
}
The short answer was RTFM! (more carefully). I am leaving my original question and code as is, and adding the solution here.
Thanks to this post for helping me figure this out: http://szulctomasz.com/ios-second-try-to-nsoperation-and-long-running-tasks/ And of course, as I found, there is no substitute for a careful reading of the Reference: https://developer.apple.com/library/ios/documentation/Cocoa/Reference/NSOperation_class/
The problem was that I was not subclassing NSOperation properly for running concurrent operations. One must add the concurrent operations to start() rather than main() and then use KVO to update the finished and executing properties, which are what signals that an operation is complete.
I needed to modify the class above to include:
private var _executing = false
private var _finished = false
override var executing:Bool{return _executing}
override var finished:Bool{return _finished}
override func cancel() {
super.cancel()
finish()
}
override func start() {
if cancelled{
finish()
return
}
willChangeValueForKey("isExecuting")
_executing = true
didChangeValueForKey("isExecuting")
getSumStatistics { (hkSamples, statsError) -> Void in
println("LoadStatistics getStatistics completion: \(hkSamples.count) samples")
self.addSamplesToArray(hkSamples, completionHandler: { (success) -> Void in
println("LoadStatistics addSamplesToArray completion")
self.finish()
})
self.completion(true)
}
main()
}
func finish(){
willChangeValueForKey("isExecuting")
willChangeValueForKey("isFinished")
_executing = false
_finished = true
didChangeValueForKey("isExecuting")
didChangeValueForKey("isFinished")
}
override func main() {
if cancelled == true && _finished != false{
finish()
return
}
}
Now I get!
cycleOperation start
LoadStatistics getStatistics completion: 97 samples
LoadStatistics addSamplesToArray completion
cycleOperation CompletionBlock
dependentOperation start
dependentOperation CompletionBlock

Resources