I do a Parse query in my iPhone app, but I am getting errors trying to do the same Parse query in my Watch app.
This is the query in my iPhone app:
- (void)viewDidLoad {
// GMT Date from Phone
NSDate *gmtNow = [NSDate date];
NSLog(#"GMT Now: %#", gmtNow);
// Query Parse
PFQuery *query = [self queryForTable];
[query whereKey:#"dateGame" greaterThanOrEqualTo:gmtNow];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
NSMutableArray *localMatchup = [#[] mutableCopy];
for (PFObject *object in objects) {
// Add objects to local Arrays
[localMatchup addObject:[object objectForKey:#"matchup"]];
// App Group
NSString *container = #"group.com.me.off";
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:container];
// Matchup
[defaults setObject:localMatchup forKey:#"KeyMatchup"];
NSArray *savedMatchup = [defaults objectForKey:#"KeyMatchup"];
NSLog(#"Default Matchup: %#", savedMatchup);
savedMatchup = matchupArray;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}];
}
This is the query I tried in my WatchKit app...
- (void)awakeWithContext:(id)context {
// GMT Date from Phone
NSDate *gmtNow = [NSDate date];
NSLog(#"GMT Now: %#", gmtNow);
// Query Parse
PFQuery *query = [self queryForTable];
[query whereKey:#"dateGame" greaterThanOrEqualTo:gmtNow];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
NSMutableArray *localMatchup = [#[] mutableCopy];
for (PFObject *object in objects) {
// Add objects to local Arrays
[localMatchup addObject:[object objectForKey:#"matchup"]];
// App Group
NSString *container = #"group.com.me.off";
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:container];
// Matchup
[defaults setObject:localMatchup forKey:#"KeyMatchup"];
NSArray *savedMatchup = [defaults objectForKey:#"KeyMatchup"];
NSLog(#"Default Matchup: %#", savedMatchup);
savedMatchup = self.matchupArray;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}];
}
But I get errors on these two lines...
`PFQuery *query = [self queryForTable];`
`[self.tableView reloadData];`
because I can't do the same exact code related to a table I'm guessing, but I'm just not sure what to change it to.
EDIT: Adding code per #cnoon answer
WatchKit InterfaceController.m:
How would I ask for my query to run here?
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
[WKInterfaceController openParentApplication:nil reply:^(NSDictionary *replyInfo, NSError *error) {
// What to put here?
NSLog(#"Open Parent Application");
}];
-and-
iPhone AppDelegate.h
How would I ask for my PFQuery to run?
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
// What to put here?
}
There's no such thing as a UITableView in a WatchKit application. Instead, you have to work with WKInterfaceTable. Before you continue, I'd also suggest you read through the documentation in the WatchKit Programming Guide. It will give you a MUCH better understanding of all the toolsets available to you as an aspiring Apple Watch developer.
WKInterfaceTable
Once you know the ins and outs of a WKInterfaceTable, you'll quickly see why your approach is flawed for two reasons. First off, you don't have a reloadData method. The alternative in WatchKit is to setNumberOfRows(_:withRowTypes:). Then you need to iterate through each row and configure it.
PFQuery in WatchKit Extension
The second reason you are going to have issues is due to your use of PFQuery.
This is a bit of side advice, so take it or leave it. I'm speaking from experience here having already built a very large page-based Watch App that heavily communicates with the iOS App.
I would advise you to stop making PFQuerys in your WatchKit Extension. The reason is that users using your Watch App are only going to have the app open for a second or two. Everything will happen extremely fast. Because of this, it is extremely difficult to guarantee the success of network calls before the Watch App is terminated by the user. This makes things MUCH more difficult, but is simply the way it is.
Instead, you want to run your PFQuery calls on the iOS App and return that information back to the Watch Extension through the following calls:
WKInterfaceController - openParentApplication(_:reply:)
UIApplicationDelegate - handleWatchKitExtensionRequest(_:reply:)
You can also cache the PFQuery into the shared app group using MMWormhole or a similar approach alternative. Below is an example of how you can have your Watch Extension request the iOS Application to run a PFQuery, cache the data in MMWormhole and notify the Watch Extension once it is finished. By always reading the data out of the cache, you have a consistent mechanism whether the Watch Extension was still running as well as closed and re-opened.
Objective-C
InterfaceController.m
- (void)willActivate {
[super willActivate];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[WKInterfaceController
openParentApplication:#{#"pfquery_request": #"dumm_val"}
reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"User Info: %#", replyInfo);
NSLog(#"Error: %#", error);
if ([replyInfo[#"success"] boolValue]) {
NSLog(#"Read data from Wormhole and update interface!");
}
}];
});
}
AppDelegate.m
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply {
if (userInfo[#"pfquery_request"]) {
NSLog(#"Starting PFQuery"); // won't print out to console since you're running the watch extension
// 1. Run the PFQuery
// 2. Write the data into MMWormhole (done in PFQuery completion block)
// 3. Send the reply back to the extension as success (done in PFQuery completion block)
reply(#{#"success": #(YES)});
}
reply(#{#"success": #(NO)});
}
Swift
InterfaceController.swift
override func willActivate() {
super.willActivate()
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2.0 * Float(NSEC_PER_SEC))), dispatch_get_main_queue()) {
WKInterfaceController.openParentApplication(["pfquery_request": "dummy_val"]) { userInfo, error in
println("User Info: \(userInfo)")
println("Error: \(error)")
if let success = (userInfo as? [String: AnyObject])?["success"] as? NSNumber {
if success.boolValue == true {
println("Read data from Wormhole and update interface!")
}
}
}
return
}
}
AppDelegate.swift
func application(
application: UIApplication!,
handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,
reply: (([NSObject : AnyObject]!) -> Void)!)
{
if let pfqueryRequest: AnyObject = (userInfo as? [String: AnyObject])?["pfquery_request"] {
println("Starting PFQuery") // won't print out to console since you're running the watch extension
// 1. Run the PFQuery
// 2. Write the data into MMWormhole (done in PFQuery completion block)
// 3. Send the reply back to the extension as success (done in PFQuery completion block)
reply(["success": true])
}
reply(["success": false])
}
Hopefully that helps break down the complexity of having a consistent way to read data from the cache as well as offload network requests (or PFQueries) to the iOS App.
Related
I'm trying to develop and Action Extension for iOS9.1 that is supposed to query Parse for some data.
I've added, enabled and tested Parse in the extension and I'm successful at creating test objects and checking for current user.
I can not get the code inside Parse's query method
[query findObjectsInBackgroundWithBlock:^ to execute. LLDB just keeps skipping it so I'm really at a loss.
This code executes perfectly within the container app so I'm a bit confused.
- (void)viewDidLoad
{
[super viewDidLoad];
[Parse enableLocalDatastore];
[Parse enableDataSharingWithApplicationGroupIdentifier:#"group.com.app.slinky"
containingApplication:#"com.app.Slinky"];
[Parse setApplicationId:#"xxxxx"
clientKey:#"xxxxx"];
for (NSExtensionItem *item in self.extensionContext.inputItems) {
for (NSItemProvider *itemProvider in item.attachments) {
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePropertyList]) {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *jsDict, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSDictionary *jsPreprocessingResults = jsDict[NSExtensionJavaScriptPreprocessingResultsKey];
NSString *pageTitle = jsPreprocessingResults[#"title"];
NSString *pageURL = jsPreprocessingResults[#"URL"];
if ([pageURL length] > 0) {
self.siteURL.text = pageURL;
self.URLstring = pageURL;
}
if ([pageTitle length] > 0) {
self.siteTitle.text = pageTitle;
}
});
}];
break;
}
}
}
[self queryParse];
}
-(void)queryParse{
PFQuery *query = [self.friendsRelation query];
[query orderByAscending:#"username"];
**[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
NSLog(#"%# %#", error, [error userInfo]);
} else {
self.friends = objects;
[self.tableView reloadData];
}
}];**
}
There are some limit to fetch Objects from Parse. Read Here
One has a limit of obtaining 100 objects, but anything from 1 to 1000 has a valid limit.
You have to set the limit again for the query for next objects and skip the properties of PFQuery.
You will have to query again and again until you reach the total count.
For Example - If you have 3000 objects to be fetched, then you have to query 3 times with a count of say 1000.
OR
Call [self queryParse]; inside dispatch_async(dispatch_get_main_queue(), ^{ }); block.
Hope this might workout.
This was just a result of a 12 hour refractoring marathon :-(
When I moved the queryParse call into its own method. I lost the definition of
self.friendsRelation = [[PFUser currentUser] objectForKey:#"friendsRelation"];
essentially sending a bad query... I don't know why it isn't return an error but regardless I can check that off now.
Thank you all for you help!
I'm getting stuck with an error when using my Watchkit Application. When I launch it, I ask the containing iOS app to get some data from network. The problem is that I get an error saying the containing app never calls 'reply()' :o But looking at my code, it should call it.
I tried to debug every step from openParentApplication to the 'reply()' call, and it seems to work well =X
Here is my code in the Watchkit extension
- (void)initDiaporamasWithSuccess:(void (^)())success andFailure:(void (^)(NSError*))failure {
NSLog(#"Ask to load diapos");
__weak typeof(self) weakSelf = self;
[WKInterfaceController openParentApplication:#{#"watchKit": #"watchKit.initDiapos"} reply:^(NSDictionary *replyInfo, NSError *error) {
if (error) {
NSLog(#"%#", error);
if (failure) {
failure(error);
}
return;
}
NSLog(#"got items : %#", replyInfo[#"diapos"]);
weakSelf.diaporamas = replyInfo[#"diapos"];
[weakSelf setDiaporama:replyInfo[#"firstDiapo"] AtIndex:0];
if (success) {
success();
}
}];
}
The result should be an NSDictionary containing an NSArray with some diaporamas basic informations, and an object (Diapo) containing the full informations of the first diaporama (e.g. self.diaporamas[0])
And here is the code in the containing app's AppDelegate :
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply {
// Maybe we could handle multiple watchKit extension calls that way ?
// Something like a key-value 'protocol' to run the right block of code
NSString *watchKitCall = userInfo[#"watchKit"];
NSLog(#"watchKit handled");
if ([watchKitCall isEqualToString:#"watchKit.initDiapos"]) {
[AppDelegate watchInitialObjects:^(NSDictionary *info) {
NSLog(#"Managed to get initial infos");
reply(info);
} failure:^(NSError *error) {
NSLog(#"Fail : %#", error);
reply(#{#"error": error});
}];
}
}
+ (void) watchInitialObjects:(void (^)(NSDictionary *info))success failure:(void (^)(NSError *error))failure {
NSDictionary *parameters = #{#"site" : #(14), #"limit" : #(10)};
[AppDelegate requestDiapoListWithParams:parameters success:^(NSArray *items) {
if ([items count] == 0)
{
NSError *error = [NSError errorWithDomain:#"com.domain.app" code:404 userInfo:nil];
failure(error);
return;
}
Diapo *firstDiapo = [items firstObject];
[AppDelegate requestDiapoDetailWithDiapo:firstDiapo success:^(Diapo *diapo) {
if (!diapo)
{
NSError *error = [NSError errorWithDomain:#"com.domain.app" code:404 userInfo:nil];
failure(error);
return;
}
NSDictionary *result = #{
#"firstDiapo" : diapo,
#"diapos" : items
};
success(result);
} failure:^(NSError *error) {
failure(error);
}];
} failure:^(NSError *error) {
failure(error);
}];
}
In the watchKitHandler, I call watchInitialObjects to get the diaporamas array and the first diaporama's informations.
In the watchInitialObjects, I make a first network call to get the array, and on success, I make an other network call to get the firs diaporama informations.
To make the calls and map the JSON into objects, I use RESTKit
I really don't get what could be the error =x
UPDATE
I forgot to write the error I get, here it is :
Error Domain=com.apple.watchkit.errors Code=2 "The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]" UserInfo=0x7fcb53e12830 {NSLocalizedDescription=The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]}
And I kept trying to know why I get this error, and I think I found it:
It seems that there is a (very little) timeout to do the work in the containing app. But I mapped the JSON data I received directly in the containing app and then, send those custom objects in the reply(). But when I removed the mapping part, it worked well !
So...that's why I think that was the problem =X
Does anybody could approve my thoughts or corrects me ?
After hours of searching and testing different codes, I finally found my problem...and it's obvious when we read the Apple documentation about 'application:handleWatchKitExtensionRequest:reply:' seriously...
here is the answer : (it's in the documentation)
The contents of the dictionary must be serializable to a property list file.
Which means that objects can ONLY be dictionaries, arrays, strings, numbers (integer and float), dates, binary data, or Boolean values
...I feel dumb ><
Using a Background Fetch we trigger a method
- (void)sendPush:(SPUserInfo *)userInfo {
PFQuery *findUserQuery = [PFUser query];
[findUserQuery whereKey:#"objectId" equalTo:userInfo.userID];
[findUserQuery findObjectsInBackgroundWithBlock:^(NSArray* arr, NSError* err) {
if (err) {
MLog(#"Error Finding User to Spark: %#", err.description);
}
else {
if (arr.count) {
MLog(#"User found");
PFObject *spark = [PFObject objectWithClassName:kSparkClassName];
[...]
[spark saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (!error) {
//save
// send push through Parse
[...]
PFPush *push = [PFPush push];
NSString *channel = [NSString stringWithFormat:#"u_%#", receiver.objectId];
[push setChannels:#[channel]];
[push setData:data];
[push sendPushInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (nil == error) {
MLog(#"Push success");
//do the stuff
[self.queueSendingSpark removeObject:userInfo];
[self saveSendingSparkQueue];
}
else {
MLog(#"Push Error : %#", error.description);
}
}];
}
else {
MLog(#"Spark is not saved on parse | %#", error);
}}];
}
else {
MLog(#"ZERO Users");
}
}
}];
}
This is all fine and dandy if the app is running in the foreground or if the phone is awake. But when the iPhone is locked/asleep, the findObjectsInBackgroundWithBlock never returns any data.
Is this expected behaviour? Is there a way to ensure this query, and others, are returning as normal when a device is asleep?
So theres a property that you can use to handle this...
#property (nonatomic, assign) UIBackgroundTaskIdentifier fileUploadBackgroundTaskId;
Within your init method set the property like so:
self.fileUploadBackgroundTaskId = UIBackgroundTaskInvalid;
Set the value when needed
// Request a background execution task to allow us to finish uploading the photo even if the app is backgrounded
self.fileUploadBackgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:self.fileUploadBackgroundTaskId];
}];
Run your query, then when the query is finished...
[[UIApplication sharedApplication] endBackgroundTask:self.fileUploadBackgroundTaskId];
We're currently trying to get HealthKit to work in the background, in order to deliver steps data to our server when the App is closed.
For experimental purposes we've created a brand new iOS project in XCode, enabled HealhtKit and all background modes in Compabilities. After that, we pretty much run the code (see further down).
So what happens first is that the app ofcourse asks for the permissions, which we grant. What we're expecting is that the app should keep deliver the steps data every hour, to the server. But it doesnt do that, it seems like the app cant do anything when it's not active.
The app only deliver data when it gets resumed or started, but not at all from the background (Soft-closed / Hard-closed)
appdelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self setTypes];
return YES;
}
-(void) setTypes
{
self.healthStore = [[HKHealthStore alloc] init];
NSMutableSet* types = [[NSMutableSet alloc]init];
[types addObject:[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]];
[self.healthStore requestAuthorizationToShareTypes: types
readTypes: types
completion:^(BOOL success, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self observeQuantityType];
[self enableBackgroundDeliveryForQuantityType];
});
}];
}
-(void)enableBackgroundDeliveryForQuantityType{
[self.healthStore enableBackgroundDeliveryForType: [HKQuantityType quantityTypeForIdentifier: HKQuantityTypeIdentifierStepCount] frequency:HKUpdateFrequencyImmediate withCompletion:^(BOOL success, NSError *error) {
}];
}
-(void) observeQuantityType{
HKSampleType *quantityType = [HKSampleType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
HKObserverQuery *query =
[[HKObserverQuery alloc]
initWithSampleType:quantityType
predicate:nil
updateHandler:^(HKObserverQuery *query,
HKObserverQueryCompletionHandler completionHandler,
NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (completionHandler) completionHandler();
[self getQuantityResult];
});
}];
[self.healthStore executeQuery:query];
}
-(void) getQuantityResult{
NSInteger limit = 0;
NSPredicate* predicate = nil;
NSString *endKey = HKSampleSortIdentifierEndDate;
NSSortDescriptor *endDate = [NSSortDescriptor sortDescriptorWithKey: endKey ascending: NO];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType: [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]
predicate: predicate
limit: limit
sortDescriptors: #[endDate]
resultsHandler:^(HKSampleQuery *query, NSArray* results, NSError *error){
dispatch_async(dispatch_get_main_queue(), ^{
// sends the data using HTTP
[self sendData: [self resultAsNumber:results]];
});
}];
[self.healthStore executeQuery:query];
}
I found this out a little while ago when talking to someone from Apple. Apparently you can't access HK data in the background if the device is locked:
NOTE
Because the HealthKit store is encrypted, your app cannot read data
from the store when the phone is locked. This means your app may not
be able to access the store when it is launched in the background.
However, apps can still write data to the store, even when the phone
is locked. The store temporarily caches the data and saves it to the
encrypted store as soon as the phone is unlocked.
from:
https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/
I see something that might be causing an issue in your AppDelegate, particularly this line:
[[NSURLConnection alloc] initWithRequest:request delegate:self];
This is creating an NSURLConnection, but not starting it. Try changing it to this:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
Edit: After taking a second look at the docs
They recommend setting up your observer queries in your application didFinishLaunchingWithOptions: method. In your code above, you set the HKObserverQuery up in the authorization handler, which is called on a random background queue. Try making this change to set it up on the main thread:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self setTypes];
[self observeQuantityType];
return YES;
}
HKObserverQuery Reference
I am trying to return a bool value from a class method I call that has a block in it. I get the error, Incompatible block pointer types sending.... How would I get around this? I just want to know if the class method I call completes with or without error...
+ (BOOL)saveSelectedDepartmentsToParse:(NSMutableDictionary *)dictionary {
NSArray *array = [dictionary allKeysForObject:#"YES"];
NSMutableArray *trimmedArray = [[NSMutableArray alloc] init];
for (NSString *string in array) {
NSString *final = [string removeAllInvalidCharacters];
[trimmedArray addObject:final];
}
NSLog(#"Array = %#", trimmedArray);
PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation removeObjectForKey:#"channels"];
[currentInstallation addObjectsFromArray:trimmedArray forKey:#"channels"];
[currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (error == nil) {
NSLog(#"Parse Save Succeeded");
[self saveDepartmentsDictionary:dictionary];
}
else {
NSLog(#"Parse Save Failed, %#", error.localizedDescription);
}
}];
}
I just want to know if the class method I call
This is a misunderstanding of how asynchronous code works. When you are supplying a block to saveInBackgroundWithBlock:, that code is not executed straight away. It's executed at some later point by the Parse framework, and whichever part of Parse that does so would get the return value if the block were defined to have one, which it isn't. Your block isn't executed at the point at which you write it, so you can't return anything at the point at which you write it.
Your code isn't calling the block, and you can't return values to your code from it. It doesn't make sense to do so. If another part of your code wants to know when the saving has finished, you'll need to use some other mechanism than return values, such as calling a method from your block, posting a notification, or Key-Value Observing.
From the block keyword InBackground:
[currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (error == nil) {
NSLog(#"Parse Save Succeeded");
[self saveDepartmentsDictionary:dictionary];
} else {
NSLog(#"Parse Save Failed, %#", error.localizedDescription);
}
}];
I guess the block is called asynchronously.
If you want to get the result, you can wait here until the block is executed, but this make the saveInBackgroundWithBlock useless.
So NSNotification may be better:
[currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (error == nil) {
NSLog(#"Parse Save Succeeded");
[self saveDepartmentsDictionary:dictionary];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationParseSaveSucceeded object:nil];
} else {
NSLog(#"Parse Save Failed, %#", error.localizedDescription);
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationParseSaveFailed object:nil];
}
}];