iOS: How to add multiple day data into Health Kit Store - ios

I'm looking into Research Kit and Health Kit. Testing wise on the simulator I don't have any data - massive issue :)
How would one get data (most specifically, steps) into the HealthKit Store? Below is what I got, which doesn't appear to fail but nothing is shown within the Health app dashboard. I want this to provide 14 days worth of data...
var date:NSDate! = NSDate()
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)
let dateComponents = NSDateComponents()
dateComponents.day = -13
let endingDate:NSDate! = calendar.dateByAddingComponents(dateComponents, toDate: date, options: NSCalendarOptions())
while date.compare(endingDate) != NSComparisonResult.OrderedAscending {
let countRate = Double(arc4random_uniform(500) * arc4random_uniform(10))
let stepsUnit = HKUnit.countUnit()
let quantity = HKQuantity(unit: stepsUnit, doubleValue: countRate)
let dateComponents = NSDateComponents()
dateComponents.day = -1
date = calendar.dateByAddingComponents(dateComponents, toDate: date, options: NSCalendarOptions())
let sample = HKQuantitySample(type: HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!, quantity: quantity, startDate: date, endDate: date)
HKHealthStore().saveObject(sample, withCompletion: { (success, error) -> Void in
if let _ = error {
print("Error saving sample: \(error!.localizedDescription)")
}
})
}

Ignore me this appears to work. You need to go into "Health Data" tab and basically select whatever you're after (in this case Fitness > Steps) and it'll show you all the data!
You can also select the tab to show on dashboard too so the initial launch of the app won't look empty!
I'll leave this here for anyone that wants a mini code sample + if they come across not realising where the data can be displayed!

Related

Unwrapping an Optional value in swift and realm

I wrote a working function for the application, but the error came out "The nil value was unexpectedly found when an optional value was implicitly deployed" limit Limit label.the text I can't fix.
Properties:
#IBOutlet weak var limitLabel: UILabel!
Function:
func leftLabels(){
let limit = self.realm.objects(Limit.self)
guard limit.isEmpty == false else {return}
limitLabel.text = limit[0].limitSum //Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
let calendar = Calendar.current
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd HH:mm"
let firstDay = limit[0].limitDate as Date
let lastDay = limit[0].limitLastDate as Date
let firstComponent = calendar.dateComponents([.year, .month, .day], from: firstDay)
let lastComponent = calendar.dateComponents([.year, .month, .day], from: lastDay)
let startDate = formatter.date(from: "\(firstComponent.year!)/\(firstComponent.month!)/\(firstComponent.day!) 00:00")
let endDate = formatter.date(from: "\(lastComponent.year!)/\(lastComponent.month!)/\(lastComponent.day!) 23:59")
let filterLimit: Int = realm.objects(SpendingDB.self).filter("self.date >= %# && self.date <= %#", startDate ?? "", endDate ?? "").sum(ofProperty: "cost")
ForThePeriod.text = "\(filterLimit)"
let a = Int(limitLabel.text!)!
let b = Int(ForThePeriod.text!)!
let c = a - b
availableForSpending.text = "\(c)"
I will be glad if you tell me the correct code
As from comments if appears that your view is not yet loaded and some of your views are still nil. Your app crashes because in line limitLabel.text = limit[0].limitSum the limitLabel is nil. It would crash regardless of Realm even by calling limitLabel.text = "Hello world!"
You can always guard data that you need to avoid changes in your code. Simply add
guard let limitLabel = limitLabel else { return nil }
guard let ForThePeriod = ForThePeriod else { return nil }
and so on.
I tried to clean up your code a bit. It is hard to understand what exactly are you trying to achieve but something like the following may seem a bit more appropriate:
func leftLabels() {
// Elements needed for method to execute.
guard let limitLabel = limitLabel else { return }
guard let forThePeriodLabel = forThePeriodLabel else { return }
guard let availableForSpendingLabel = availableForSpendingLabel else { return }
// Items that will be reused throughout the method later on
let limits: [Limit]
let firstLimit: Limit
let dates: (start: Date?, end: Date?)
let filterLimit: Int
limits = self.realm.objects(Limit.self)
guard limits.isEmpty == false else { return }
firstLimit = limits[0]
// limitLabel
limitLabel.text = firstLimit.limitSum
// Date components
dates = {
let calendar = Calendar.current
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd HH:mm"
let firstDay = firstLimit.limitDate as Date
let lastDay = firstLimit.limitLastDate as Date
let firstComponent = calendar.dateComponents([.year, .month, .day], from: firstDay)
let lastComponent = calendar.dateComponents([.year, .month, .day], from: lastDay)
let startDate = formatter.date(from: "\(firstComponent.year!)/\(firstComponent.month!)/\(firstComponent.day!) 00:00")
let endDate = formatter.date(from: "\(lastComponent.year!)/\(lastComponent.month!)/\(lastComponent.day!) 23:59")
return (startDate, endDate)
}()
// forThePeriodLabel
filterLimit = realm.objects(SpendingDB.self).filter("self.date >= %# && self.date <= %#", startDate ?? "", endDate ?? "").sum(ofProperty: "cost")
forThePeriodLabel.text = String(filterLimit)
// availableForSpendingLabel
availableForSpendingLabel.text = {
guard let a = Int(firstLimit.limitSum) else { return "" }
let b = filterLimit
let c = a - b
return String(c)
}()
}
Note some practices which help you better to structure and solve your code.
Guard dangerous data at first
Create a list of reusable items for your method (there should be as fewer as possible, in most cases none). Note how these can be later assigned to. And if you try using it before assigning to it, you will be warned by your compiler.
Wrap as much code into closed sections such as availableForSpendingLabel.text = { ... code here ... }()
Use tuples such as let dates: (start: Date?, end: Date?)
Don't be afraid of using long names such as availableForSpendingLabel
I would even further try and break this down into multiple methods. But I am not sure what this method does and assume that you have posted only part of it...
========== EDIT: Adding alternate approach ==========
From comments this is a financial application so probably at least dealing with Decimal numbers would make sense. Also introducing approach with adding a new structure which resolves data internally. A formatter is also used to format the number. And some other improvements:
struct Limit {
let amount: Decimal
let startDate: Date
let endDate: Date
}
struct Spending {
let cost: Decimal
let date: Date
}
struct LimitReport {
let limitAmount: Decimal
let spendingSum: Decimal
let balance: Decimal
init(limit: Limit) {
let limitAmount: Decimal = limit.amount
let spendingSum: Decimal = {
let calendar = Calendar.autoupdatingCurrent // Is this OK or should it be some UTC or something?
func beginningOfDate(_ date: Date) -> Date {
let components = calendar.dateComponents([.day, .month, .year], from: date)
return calendar.date(from: components)!
}
let startDate = beginningOfDate(limit.startDate)
let endDate = calendar.date(byAdding: .day, value: 1, to: startDate)
let spendings: [Spending] = realm.objects(Spending.self).filter { $0.date >= startDate && $0.date < endDate }
return spendings.reduce(0, { $0 + $1.cost })
}()
let balance = limitAmount - spendingSum
self.limitAmount = limitAmount
self.spendingSum = spendingSum
self.balance = balance
}
}
func leftLabels() {
// Elements needed for method to execute.
guard let limitLabel = limitLabel else { return }
guard let forThePeriodLabel = forThePeriodLabel else { return }
guard let availableForSpendingLabel = availableForSpendingLabel else { return }
guard let limit = self.realm.objects(Limit.self).first else { return }
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "$"
let report = LimitReport(limit: limit)
limitLabel.text = formatter.string(from: report.limitAmount)
forThePeriodLabel.text = formatter.string(from: report.spendingSum)
availableForSpendingLabel.text = formatter.string(from: report.balance)
}
Matic provided a good, comprehensive answer to your question (voted). I thought I'd provide an answer narrowly focused on your crash and a "short and sweet" way to fix it:
The line in question could crash 2 different ways:
limitLabel.text = limit[0].limitSum //Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
Your limitLabel IBOutlet is declared as an "implicitly unwrapped Optional" (Note the ! after the type, UILabel:
#IBOutlet weak var limitLabel: UILabel!
An implicitly unwrapped Optional is an Optional where, essentially, the compiler adds a hidden "!" force-unwrap every time you try to reference that object.
That means that
limitLabel.text = //something
Is compiled as
limitLabel!.text = //something
and if limitLabel is nil, you crash.
If you call your leftLabels() function before your view has been loaded, or if that outlet is never connected, you will crash.
You can fix that by adding an optional unwrap to the statement:
limitLabel?.text = //something
(That construct is known as "optional chaining".)
Given that the crash message you're getting mentions "implicitly unwrapping an Optional value" it's likely that that is what is crashing in your case. However, you should fix the other issue as well.
The second way you can crash is in your array indexing.
limitLabel.text = limit[0].limitSum
When you fetch an object from an array by index, your app will crash if the array does not contain an item at that index. The expression limit[0] will crash if the limit array is empty.
The array type has a computed property first that will return an optional if the array is empty.
You should change that to limit.first?.limitSum.
Change the whole line to be:
limitLabel?.text = limit.first()?.limitSum
And it won't crash any more.

iOS HealthKit Project using HKActivitySummaryQuery - Summaries Always Nil

Objective: Read from HealthKit My Activity Data ( i.e. activity ring data - move calories, workout minutes, and stands )
Problem Experienced: Result Handler returns 'nil' HKActivitySummary Array
What I have Attempted:
I have added the HealthKit Capability to the project and added the two info.plist requirements:
Privacy - Health Share Usage Description
Privacy - Health Update Usage Description
And ensured I tapped "allow all" for permissions when the permission request popup appears for HealthKit within the iPhone.
I also have many weeks of activity data recorded so that should also be fine.
How can I retrieve the array of HKActivitySummary objects in order to replicate the rings within my iOS app.
My Code:
override func viewDidLoad() {
super.viewDidLoad()
startQueryForActivitySummary(view: self.view)
}
func startQueryForActivitySummary(view: UIView) {
let calendar = NSCalendar.current
let endDate = Date()
guard let startDate = calendar.date(byAdding: .day, value: -7, to: endDate) else {
fatalError("*** Unable to create the start date ***")
}
let units: Set<Calendar.Component> = [.day, .month, .year, .era]
var startDateComponents = calendar.dateComponents(units, from: startDate)
startDateComponents.calendar = calendar
var endDateComponents = calendar.dateComponents(units, from: endDate)
endDateComponents.calendar = calendar
let queryPredicate = HKQuery.predicate(forActivitySummariesBetweenStart: startDateComponents,
end: endDateComponents)
let query = HKActivitySummaryQuery(predicate: queryPredicate) { (query, summaries, error) -> Void in
if let summaries = summaries { // print(summaries) before this line will always return nil.
if let summary = summaries.first {
let activeEnergyBurned = summary.activeEnergyBurned.doubleValue(for: HKUnit.kilocalorie())
let activeEnergyBurnedGoal = summary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie())
let activeEnergyBurnGoalPercent = round(activeEnergyBurned/activeEnergyBurnedGoal)
let frame = CGRect(x: 0, y: 0, width: 200, height: 200)
let ringView = HKActivityRingView(frame: frame)
view.addSubview(ringView)
ringView.setActivitySummary(summary, animated: true)
}
}
}
let allTypes = Set([HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!])
let healthStore = HKHealthStore()
healthStore.requestAuthorization(toShare: allTypes, read: allTypes) { (success, error) in
healthStore.execute(query)
}
}
I think you are missing permission HKObjectType.activitySummaryType()
Extend you types like this:
let allTypes = Set([
HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.activitySummaryType()
])
Here is the documentation: https://developer.apple.com/documentation/healthkit/hkobjecttype/1615319-activitysummarytype

How to differentiate sources with HealthKit sleep query

I'm currently using the following code to query for the number of hours the user was asleep in the last 24 hours:
func getHealthKitSleep() {
let healthStore = HKHealthStore()
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
// Get all samples from the last 24 hours
let endDate = Date()
let startDate = endDate.addingTimeInterval(-1.0 * 60.0 * 60.0 * 24.0)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
// Sleep query
let sleepQuery = HKSampleQuery(
sampleType: HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis)!,
predicate: predicate,
limit: 0,
sortDescriptors: [sortDescriptor]){ (query, results, error) -> Void in
if error != nil {return}
// Sum the sleep time
var minutesSleepAggr = 0.0
if let result = results {
for item in result {
if let sample = item as? HKCategorySample {
if sample.value == HKCategoryValueSleepAnalysis.asleep.rawValue && sample.startDate >= startDate {
let sleepTime = sample.endDate.timeIntervalSince(sample.startDate)
let minutesInAnHour = 60.0
let minutesBetweenDates = sleepTime / minutesInAnHour
minutesSleepAggr += minutesBetweenDates
}
}
}
self.sleep = Double(String(format: "%.1f", minutesSleepAggr / 60))!
print("HOURS: \(String(describing: self.sleep))")
}
}
// Execute our query
healthStore.execute(sleepQuery)
}
This works great if the user has only one sleep app as the source for the data. The problem is if the user is using 2 sleep apps, for example, as sources, the data will be doubled. How can I differentiate the sources? If able to differentiate the sources, I would like to either only grab data from one source, or maybe take the average of the sources.
When you're looping over the samples, you can access information about the source for each. I only accept a single source, so I just keep a variable of the source name and if the current sample has a different source name I continue looping without processing the data from that sample, but you could combine the data in other ways if you wanted to.
Here's how to access the source info:
if let sample = item as? HKCategorySample {
let name = sample.sourceRevision.source.name
let id = sample.sourceRevision.source.bundleIdentifier
}
There's some more info on the HKSourceRevision object in the docs here.

Swift - Exercise minutes Healthkit

So I have been trying to get the exercise minutes from Healthkit onto my application and store it in a variable.
But the whenever the application is opened on an Iphone connected to an apple wathc, it crashes. I have tried debugging the application, but it works fine on a simulator or my Ipod touch. This the function I am using to retrieve the exercise minutes.
func getExerciseTime(completion: #escaping (Double) -> Void) {
let exerciseQuantityType = HKQuantityType.quantityType(forIdentifier: .appleExerciseTime)!
/*
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
*/
var now : Date
var startOfDay : Date
var predicate : NSPredicate
switch dwmValue {
case 0:
now = Date()
startOfDay = Calendar.current.startOfDay(for: now)
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
case 1:
now = Date()
startOfDay = Calendar.current.startOfDay(for: Date(timeIntervalSinceNow: -60 * 60 * 24 * 7))
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
case 2:
now = Date()
let wx = -60 * 60 * 24 * 2
startOfDay = Calendar.current.startOfDay(for: Date(timeIntervalSinceNow: TimeInterval((-60 * 60 * 24 * 7 * 4) + wx)))
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
default:
now = Date()
startOfDay = Calendar.current.startOfDay(for: now)
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
}
let query = HKStatisticsQuery(quantityType: exerciseQuantityType, quantitySamplePredicate: predicate, options: .cumulativeSum ) { (_, result, error) in
var resultCount = 0.0
guard let result = result else {
//log.error("Failed to fetch steps = \(error?.localizedDescription ?? "N/A")")
completion(resultCount)
return
}
if let sum = result.sumQuantity() {
resultCount = sum.doubleValue(for: HKUnit.count())
}
DispatchQueue.main.async {
completion(resultCount)
print("Exercise time : \(resultCount)")
}
}
healthKitStore.execute(query)
}
This is the code I use in viewdidAppear to store the value from the above function in a global variable
getExerciseTime(){ time in
exerciseTime = time
}
I have no idea why the application keeps crashing on the Iphone. I have tried to change the options in StatisticsQuery but nothing has worked. Please help me out here!! And I know there is no problem with healthkit authentication as it return some data on the simulator and the iPod but crashes on the Iphone that is connected to an apple watch.
When you are summing the quantities, you are using an incompatible type (HKUnit.count()), you need to use a time unit.
resultCount = sum.doubleValue(for: HKUnit.minute())
Also, if you are not doing so already, you need to ask for permission to read
override func viewDidAppear(_ animated: Bool) {
healthKitStore.requestAuthorization(toShare: nil, read: [exerciseQuantityType], completion: { (userWasShownPermissionView, error) in
self.getExerciseTime(){ time in
self.exerciseTime = time
}
})
}
You need to set a usage description in your plist
<key>NSHealthShareUsageDescription</key>
<string>Foo</string>
Aslo your app needs HealthKit capability set in the project target settings.

Solve memory issue with migrating a big bunch of data

In my previous version of my app I stored all user data into .dat files (with NSKeyedArchiver), but in my new version I want to upgrade to a real(m) database.
I'm trying to import all of this data (and that can be a LOT) into Realm. But it's taking so much memory that the debugger eventually kill my app before the migration has finished. The 'strange' thing is that the data on hard disk is only 1.5 mb big, but it's taking memory for more than 1gb so I'm doing something wrong.
I also tried to work with multiple threads, but that didn't help. Well it speeded up the migration process (which is good), but it also took the same amount of memory.
Who can help me out? See my code below for more information..
FYI Async can be found here https://github.com/duemunk/Async
import Async
let startDate = NSDate(timeIntervalSince1970: 1388534400).startOfDay // Start from 2014 jan 1st
let endDate = NSDate().dateByAddingTimeInterval(172800).startOfDay // 2 days = 3600 * 24 * 2 = 172.800
var pathDate = startDate
let calendar = NSCalendar.currentCalendar()
let group = AsyncGroup()
var allPaths = [(Int, Int)]()
while calendar.compareDate(pathDate, toDate: endDate, toUnitGranularity: .Month) != .OrderedDescending {
// Components
let currentMonth = calendar.component(.Month, fromDate: pathDate)
let currentYear = calendar.component(.Year, fromDate: pathDate)
allPaths.append((currentYear, currentMonth))
// Advance by one month
pathDate = calendar.dateByAddingUnit(.Month, value: 1, toDate: pathDate, options: [])!
}
for path in allPaths {
group.background {
// Prepare path
let currentYear = path.0
let currentMonth = path.1
let path = (Path.Documents as NSString).stringByAppendingPathComponent("Stats_\(currentMonth)_\(currentYear).dat")
print(path)
if NSFileManager.defaultManager().fileExistsAtPath(path) {
NSKeyedUnarchiver.setClass(_OldStatisticsDataModel.self, forClassName: "StatisticsDataModel")
if let statistics = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? [_OldStatisticsDataModel] {
// Loop through days
for i in 1...31 {
let dateComponents = NSDateComponents()
dateComponents.year = currentYear
dateComponents.month = currentMonth
dateComponents.day = i
dateComponents.hour = 0
dateComponents.minute = 0
// Create date from components
let userCalendar = NSCalendar.currentCalendar() // user calendar
guard let date = userCalendar.dateFromComponents(dateComponents) else {
continue
}
// Search for order items
let filtered = statistics.filter {
if let date = $0.date {
let dateSince1970 = date.timeIntervalSince1970
return date.startOfDay.timeIntervalSince1970 <= dateSince1970 && date.endOfDay.timeIntervalSince1970 >= dateSince1970
}
return false
}
if filtered.isEmpty == false {
// Create order
let transaction = Transaction()
transaction.employee = Account.API().administratorEmployee()
let order = Order()
order.status = PayableStatus.Paid
order.createdDate = date.timeIntervalSince1970
order.paidDate = date.timeIntervalSince1970
// Loop through all found items
for item in filtered {
// Values
let price = (item.price?.doubleValue ?? 0.0) * 100.0
let tax = (item.tax?.doubleValue ?? 0.0) * 100.0
// Update transaction
transaction.amount += Int(price)
// Prepare order item
let orderItem = OrderItemm()
orderItem.amount = item.amount
orderItem.price = Int(price)
orderItem.taxPercentage = Int(tax)
orderItem.name = item.name ?? ""
orderItem.product = Product.API().productForName(orderItem.name, price: orderItem.price, tax: orderItem.taxPercentage)
// Add order item to order
order.orderItems.append(orderItem)
}
if order.orderItems.isEmpty == false {
print("\(date): \(order.orderItems.count) order items")
// Set transaction for order
order.transactions.append(transaction)
// Save the order
Order.API().saveOrders([order])
}
}
}
}
}
}
}
group.wait()
As in the comments of my question was suggested by #bdash, autoreleasepool did the trick.
I use AsyncSwift as syntactic sugar for grand central dispatch, but when using block groups the group keeps a reference to the block which caused that the memory wasn't released. I still make use of a group now but I leave the group after it's finished.
I provided my code example below, to make things clearer. Using multiple threads gave me incredibly (like 10 times faster) more performance while memory won't go above 150MB. Previously app was crashing at 1,3GB due to memory pressure.
let group = AsyncGroup()
var allPaths = [(Int, Int)]()
// Some logic to fill the paths -> not interesting
for path in allPaths {
group.enter() // The following block will be added to the group
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
autoreleasepool { // Creating an autoreleasepool to free up memory for the loaded statistics
// Stripped unnecessary stuff
if NSFileManager.defaultManager().fileExistsAtPath(path) {
// Load the statistics from .dat files
NSKeyedUnarchiver.setClass(_OldStatisticsDataModel.self, forClassName: "StatisticsDataModel")
if let statistics = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? [_OldStatisticsDataModel] {
// Loop through days
for i in 1...31 {
autoreleasepool {
// Do the heavy stuff here
}
}
}
}
}
group.leave() // Block has finished, now leave the group
}
}
group.wait()

Resources