Getting wrong steps history from CMPedometer - ios

Recently I face a problem which is rare to appear
What I try to do is to get the number of steps per minute.
In my code:
let date = NSDate()
for i in 0...1000 {
dispatch_async(mySerialQueue) {
self.pedoMeter.queryPedometerDataFromDate(date.dateByAddingTimeInterval( Double(i+1) * -60.0 ), toDate: date.dateByAddingTimeInterval( Double(i) * -60.0 ), withHandler: { (data, error) in
if let data = data {
print("\(data.numberOfSteps)")
}
})
}
}
Sometimes, the number of steps would return a big number which can be >1000000. After tracing the device log, I found that there is a error log:
Sep 15 16:42:59 locationd[6315] <Error>: Steps were found to be non monotonically increasing - start:488825.000000, end:488825.000000
and that is the weird step number.
I am trying to avoid the problem. That's why I am using a serial queue to do the query. However, I failed. Is there any way to avoid it?

Related

Increase time interval in Firebase HTTPs Callable iOS Swift

I'm working with my iOS Swift code, and have successfully installed all dependencies.
now, I'm trying to increase timeoutInterval in Firebase function.
functions.httpsCallable("getData").call(){ (result, error) in
guard error == nil else {
print(error)
return
}
.........
}
You cannot do it from client side. You will have to increase the timeout in your functions like thi:
const runtimeOpts = {
timeoutSeconds: 300,
memory: '1GB'
}
exports.getData = functions
.runWith(runtimeOpts)
https.onCall((data, ctx) = > {
// the function
});
The maximum value for timeoutSeconds is 540, or 9 minutes.
Detailed information can be found in the documentation
I found the answer. at first you have to set time in Firebase server, then in client side(Swift) use this code:
functions.httpsCallable("getData").timeoutInterval = 120

How can I retrieve the heart beats during a period of time using HKHeartbeatSeriesSample?

I'm trying to get the underlying heart beat data from healthStore based on this conference at the WWDC 2019 - Exploring new Data Representations in HealthKit. At mark 24:40 is explained how HealthKit has a sensor capable of storing the exact timestamps of the beats and this data is accessible through the HKHeartbeatSeriesSample type.
I've created this query to retrieve that data:
func queryData(start: Date, end: Date) {
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
let heartBeatSeriesSample = HKSampleQuery(sampleType: HKSeriesType.heartbeat(), predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) {
(query, results, error) in
guard let samples = results, let sample = samples.first as? HKHeartbeatSeriesSample else {
print("Failed: no samples collected")
return
}
print(results as Any)
let heartBeatSeriesQuery = HKHeartbeatSeriesQuery(heartbeatSeries: sample) {
(query, timeSinceSeriesStart, precededByGap, done, error) in
guard error == nil else {
print("Failed querying the raw heartbeat data: \(String(describing: error))")
return
}
print(timeSinceSeriesStart)
}
self.healthStore?.execute(heartBeatSeriesQuery)
}
healthStore?.execute(heartBeatSeriesSample)
}
But it only works if I use no predicate, which then shows all heartbeat data stored previously, but during the time period nothing is stored.
I've managed to store some data using the HKHeartbeatSeriesBuilder the following way:
let builder = HKHeartbeatSeriesBuilder(
healthStore: self.healthStore!,
device: .local(),
start: start
)
builder.addHeartbeatWithTimeInterval(sinceSeriesStartDate: 0.5, precededByGap: false) {
(success, error) in
guard success else {
fatalError("Could not add heartbeat: \(String(describing: error))")
}
}
It stored the data and not the actual beats read by the Apple Watch device. Since I would need the actual beats to store them this way, then there must be a query that gives me that data before I have to store it in the first place (classic chicken and the egg).
How can I read the exact heart beats with timestamps during a given period of time?
Yes. Unfortunately it is not possible to extract the raw data with the apple watch alone. I opened a ticket in apple developer support and they said the advertised features were possible, using a third party device. Very disappointing and for sure not chosing Apple for a serious project like this in the future.

How do I deal with multiple widget instances accessing the same CoreData store?

Background Info
I have a main application, which writes a single entry to a database contained in an App Group (we'll call them "DB1", and "Group1", respectively). To the same project, I have added an iOS 14 Home Screen Widget extension. This extension is then added to Group1.
All three sizes (small, medium, large) show the same information from the DB1 entry, just rearranged, and for the smaller widgets, some parts omitted.
Problem
The problem is, if I have multiple instances of the widget (say a small and a medium), then when I rebuild the target, and it loads/runs, I get CoreData errors such as the following:
<NSPersistentStoreCoordinator: 0x---MEM--->: Attempting recovery from error encountered during addPersistentStore: Error Domain=NSCocoaErrorDomain Code=134081 "(null)" UserInfo={NSUnderlyingException=Can't add the same store twice}
Relevant Code
Here's the code for the Timeline function
public func timeline(with context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
//Set up widget to refresh every minute
let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)!
WidgetDataSource.shared.loadTimelineEntry { entry in
guard let e = entry else { return }
let entries: [TimelineEntry] = [e]
let timeline = Timeline(entries: entries, policy: .after(refreshDate))
completion(timeline)
}
}
And here is the loadTimelineEntry function
(persistentContainer gets initialized in the init() of WidgetDataSource, the class that holds loadTimelineEntry function)
func loadTimelineEntry(callback: #escaping (TimelineEntry?) -> Void) {
persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
print("Loading persistent stores")
var widgetData: [WidgetData]
if let error = error {
print("Unresolved error \(error)")
callback(nil)
} else {
let request = WidgetData.createFetchRequest()
do {
widgetData = try self.persistentContainer.viewContext.fetch(request)
guard let data = widgetData.first else { callback(nil); return }
print("Got \(widgetData.count) WidgetData records")
let entry = TimelineEntry(date: Date())
callback(entry)
} catch {
print("Fetch failed")
callback(nil)
}
}
})
}
What I've Tried
Honestly, not much. I had gotten this same error before I put the widget into the Group1. That time it was due to the main app already having DB1 created on it's first run, then on the subsequent widget run, it looked in it's own container, didn't find it, and attempted to create it's own DB1, and the OS didn't let it due to them having the same name.
Widgets being a fairly new feature, there are not many questions regarding this specific use case, but I'm sure someone out there has a similar setup. Any help or tips will be highly appreciated!
Thanks to Tom Harrington's comment, I was able to solve the warnings by adding a check at the top of my loadTimelineEntry function like so:
if !persistentContainer.persistentStoreCoordinator.persistentStores.isEmpty {
//Proceed with fetch requests, etc. w/o loading stores
} else {
//Original logic to load store, and fetch data
}

SFSpeechRecognizer (Siri Transcription) Timeout Error on iOS App

In my iOS app, I am trying to transcribe prerecorded audio using iOS 10's latest feature, the Speech API.
Multiple sources including the documentation have stated that the audio duration limit for the Speech API (more specifically SFSpeechRecognizer) is 1 minute.
In my code, I have found that any audio files with a length of about 15 seconds or more, will get the following error.
Error Domain=kAFAssistantErrorDomain Code=203 "SessionId=com.siri.cortex.ace.speech.session.event.SpeechSessionId#50a8e246, Message=Timeout waiting for command after 30000 ms" UserInfo={NSLocalizedDescription=SessionId=com.siri.cortex.ace.speech.session.event.SpeechSessionId#50a8e246, Message=Timeout waiting for command after 30000 ms, NSUnderlyingError=0x170248c40 {Error Domain=SiriSpeechErrorDomain Code=100 "(null)"}}
I have searched all over the internet and have not been able to find a solution to this. There also have been people with the same problem. Some people suspect that it's a problem with Nuance.
It is also worth noting that I do get partial results from the transcription process.
Here's the code from my iOS app.
` // Create a speech recognizer request object.
let srRequest = SFSpeechURLRecognitionRequest(url: location)
srRequest.shouldReportPartialResults = false
sr?.recognitionTask(with: srRequest) { (result, error) in
if let error = error {
// Something wrong happened
print(error.localizedDescription)
} else {
if let result = result {
print(4)
print(result.bestTranscription.formattedString)
if result.isFinal {
print(5)
transcript = result.bestTranscription.formattedString
print(result.bestTranscription.formattedString)
// Store the transcript into the database.
print("\nSiri-Transcript: " + transcript!)
// Store the audio transcript into Firebase Realtime Database
self.firebaseRef = FIRDatabase.database().reference()
let ud = UserDefaults.standard
if let uid = ud.string(forKey: "uid") {
print("Storing the transcript into the database.")
let path = "users" + "/" + uid + "/" + "siri_transcripts" + "/" + date_recorded + "/" + filename.components(separatedBy: ".")[0]
print("transcript database path: \(path)")
self.firebaseRef.child(path).setValue(transcript)
}
}
}
}
}`
Thank you for your help.
I haven't confirmed my answer aside from someone else running into the same problem but I believe it is an undocumented limit on prerecorded audio.
Remove the result.isFinal and do a null check for the result instead. Reference: https://github.com/mssodhi/Jarvis-ios/blob/master/Jarvis-ios/HomeCell%2Bspeech.swift
This is true, I extracted the audio file from the video, and if it exceeds 15 seconds, it will give the following error:
Domain = kAFAssistantErrorDomain Code = 203 "Timeout" UserInfo = {
NSLocalizedDescription = Timeout,
NSUnderlyingError = 0x1c0647950 {Error Domain=SiriSpeechErrorDomain Code=100 "(null)"}
}
The key issue is the audio file recognition after more than 15 seconds.
result.isFinal is always 0, which is very frustrating is that there is no accurate timestamp, although it is "Timeout", it has complete recognition content, which makes me feel weird.
If you print out the result traversal, you can see that there is some restriction, which is 15 seconds, but the reason is that the timestamp feedback of the audio file is limited to a limited number, such as 15 or 4 or 9, leading to the end. Timeout feedback is more unstable.
But in real-time speech recognition, you can break through 15 seconds, as described in the official documentation, within one minute.

Handling CloudKit Errors

I am looking for general advice on handling CloudKit errors in Swift and am having trouble finding good examples online. Here are the things I'm wondering:
1) Should I account for every single error type each time the possibility for an error arises, or is that not really necessary?
2) I have read that one common way of handling CloudKit errors is by retrying to execute the operation after the time interval the error message provides. Should this retry basically be my standard procedure for all errors?
3) Do different CloudKit operations (save, fetch, etc.) produce different types of errors, or is there one standard set of CloudKit errors?
Thanks in advance! I'm just looking for general info on how to go about tackling error handling with CloudKit because I'm not really sure where to start.
Yes, you want to check every cloudkit call for errors. Apple stresses this point in the cloudkit-related WWDC videos.
What you do when you detect an error varies greatly. Retry is sometimes an option, but sometimes not appropriate. If you're using batch operations, retrying may require some additional work to extract the just the records that failed. So, yes you may sometimes want to retry, but no, you probably won't automatically retry every operation that fails.
There is one set of errors, defined in CKError.h. But, you don't always just get a CKError. Sometimes, especially with CKErrorPartialFailure, you get a top-level error that contains nested errors that you also have to unwrap. As of IOS 10, the list of errors in CKError.h looks like:
typedef NS_ENUM(NSInteger, CKErrorCode) {
CKErrorInternalError = 1, /* CloudKit.framework encountered an error. This is a non-recoverable error. */
CKErrorPartialFailure = 2, /* Some items failed, but the operation succeeded overall. Check CKPartialErrorsByItemIDKey in the userInfo dictionary for more details. */
CKErrorNetworkUnavailable = 3, /* Network not available */
CKErrorNetworkFailure = 4, /* Network error (available but CFNetwork gave us an error) */
CKErrorBadContainer = 5, /* Un-provisioned or unauthorized container. Try provisioning the container before retrying the operation. */
CKErrorServiceUnavailable = 6, /* Service unavailable */
CKErrorRequestRateLimited = 7, /* Client is being rate limited */
CKErrorMissingEntitlement = 8, /* Missing entitlement */
CKErrorNotAuthenticated = 9, /* Not authenticated (writing without being logged in, no user record) */
CKErrorPermissionFailure = 10, /* Access failure (save, fetch, or shareAccept) */
CKErrorUnknownItem = 11, /* Record does not exist */
CKErrorInvalidArguments = 12, /* Bad client request (bad record graph, malformed predicate) */
CKErrorResultsTruncated NS_DEPRECATED(10_10, 10_12, 8_0, 10_0, "Will not be returned") = 13,
CKErrorServerRecordChanged = 14, /* The record was rejected because the version on the server was different */
CKErrorServerRejectedRequest = 15, /* The server rejected this request. This is a non-recoverable error */
CKErrorAssetFileNotFound = 16, /* Asset file was not found */
CKErrorAssetFileModified = 17, /* Asset file content was modified while being saved */
CKErrorIncompatibleVersion = 18, /* App version is less than the minimum allowed version */
CKErrorConstraintViolation = 19, /* The server rejected the request because there was a conflict with a unique field. */
CKErrorOperationCancelled = 20, /* A CKOperation was explicitly cancelled */
CKErrorChangeTokenExpired = 21, /* The previousServerChangeToken value is too old and the client must re-sync from scratch */
CKErrorBatchRequestFailed = 22, /* One of the items in this batch operation failed in a zone with atomic updates, so the entire batch was rejected. */
CKErrorZoneBusy = 23, /* The server is too busy to handle this zone operation. Try the operation again in a few seconds. */
CKErrorBadDatabase = 24, /* Operation could not be completed on the given database. Likely caused by attempting to modify zones in the public database. */
CKErrorQuotaExceeded = 25, /* Saving a record would exceed quota */
CKErrorZoneNotFound = 26, /* The specified zone does not exist on the server */
CKErrorLimitExceeded = 27, /* The request to the server was too large. Retry this request as a smaller batch. */
CKErrorUserDeletedZone = 28, /* The user deleted this zone through the settings UI. Your client should either remove its local data or prompt the user before attempting to re-upload any data to this zone. */
CKErrorTooManyParticipants NS_AVAILABLE(10_12, 10_0) = 29, /* A share cannot be saved because there are too many participants attached to the share */
CKErrorAlreadyShared NS_AVAILABLE(10_12, 10_0) = 30, /* A record/share cannot be saved, doing so would cause a hierarchy of records to exist in multiple shares */
CKErrorReferenceViolation NS_AVAILABLE(10_12, 10_0) = 31, /* The target of a record's parent or share reference was not found */
CKErrorManagedAccountRestricted NS_AVAILABLE(10_12, 10_0) = 32, /* Request was rejected due to a managed account restriction */
CKErrorParticipantMayNeedVerification NS_AVAILABLE(10_12, 10_0) = 33, /* Share Metadata cannot be determined, because the user is not a member of the share. There are invited participants on the share with email addresses or phone numbers not associated with any iCloud account. The user may be able to join the share if they can associate one of those email addresses or phone numbers with their iCloud account via the system Share Accept UI. Call UIApplication's openURL on this share URL to have the user attempt to verify their information. */
} NS_ENUM_AVAILABLE(10_10, 8_0);
One approach, while you're developing and testing the app, is to check every cloudkit operation for an error, and if detected, fire an NSAssert to stop the app. Then, examine the error, underlying errors and context to determine why it failed and what you need to do about it. Most likely, over time, you'll see common patterns emerge and you can then consider building a generic error handler.
I've written a CloudKit help that makes it much easier to process errors. This is just a starting point and there is a lot more that can be done.
The main focus of this helper, in its current state, is to make it easy to retry errors that should be retried after an appropriate timeout.
But you still need to deal with errors that shouldn't be retried such as the user's iCloud storage being full. Even with this helper, every call to one of these helper methods needs to properly handle the result and possibly report an error to the user. Of course you can add a help method that checks all of the possible error types and shows an appropriate message. Then all uses of the CloudKit code can call that one helper method.
This also only covers a few of the possible operations. You would want to add support for other operations as well. Lastly, this doesn't handle partial errors yet. That would be another useful enhancement.
import Foundation
import CloudKit
public class CloudKitHelper {
private static func determineRetry(error: Error) -> Double? {
if let ckerror = error as? CKError {
switch ckerror {
case CKError.requestRateLimited, CKError.serviceUnavailable, CKError.zoneBusy, CKError.networkFailure:
let retry = ckerror.retryAfterSeconds ?? 3.0
return retry
default:
return nil
}
} else {
let nserror = error as NSError
if nserror.domain == NSCocoaErrorDomain {
if nserror.code == 4097 {
print("cloudd is dead")
return 6.0
}
}
print("Unexpected error: \(error)")
}
return nil
}
public static func modifyRecordZonesOperation(database: CKDatabase, recordZonesToSave: [CKRecordZone]?, recordZoneIDsToDelete: [CKRecordZoneID]?, modifyRecordZonesCompletionBlock: #escaping (([CKRecordZone]?, [CKRecordZoneID]?, Error?) -> Void)) {
let op = CKModifyRecordZonesOperation(recordZonesToSave: recordZonesToSave, recordZoneIDsToDelete: recordZoneIDsToDelete)
op.modifyRecordZonesCompletionBlock = { (savedRecordZones: [CKRecordZone]?, deletedRecordZoneIDs: [CKRecordZoneID]?, error: Error?) -> Void in
if let error = error {
if let delay = determineRetry(error: error) {
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
CloudKitHelper.modifyRecordZonesOperation(database: database, recordZonesToSave: recordZonesToSave, recordZoneIDsToDelete: recordZoneIDsToDelete, modifyRecordZonesCompletionBlock: modifyRecordZonesCompletionBlock)
}
} else {
modifyRecordZonesCompletionBlock(savedRecordZones, deletedRecordZoneIDs, error)
}
} else {
modifyRecordZonesCompletionBlock(savedRecordZones, deletedRecordZoneIDs, error)
}
}
database.add(op)
}
public static func modifyRecords(database: CKDatabase, records: [CKRecord], completion: #escaping (([CKRecord]?, Error?) -> Void)) {
CloudKitHelper.modifyAndDeleteRecords(database: database, records: records, recordIDs: nil) { (savedRecords, deletedRecords, error) in
completion(savedRecords, error)
}
}
public static func deleteRecords(database: CKDatabase, recordIDs: [CKRecordID], completion: #escaping (([CKRecordID]?, Error?) -> Void)) {
CloudKitHelper.modifyAndDeleteRecords(database: database, records: nil, recordIDs: recordIDs) { (savedRecords, deletedRecords, error) in
completion(deletedRecords, error)
}
}
public static func modifyAndDeleteRecords(database: CKDatabase, records: [CKRecord]?, recordIDs: [CKRecordID]?, completion: #escaping (([CKRecord]?, [CKRecordID]?, Error?) -> Void)) {
let op = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: recordIDs)
op.savePolicy = .allKeys
op.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, deletedRecordIDs: [CKRecordID]?, error: Error?) -> Void in
if let error = error {
if let delay = determineRetry(error: error) {
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
CloudKitHelper.modifyAndDeleteRecords(database: database, records: records, recordIDs: recordIDs, completion: completion)
}
} else {
completion(savedRecords, deletedRecordIDs, error)
}
} else {
completion(savedRecords, deletedRecordIDs, error)
}
}
database.add(op)
}
}

Resources