I'm developing a native application for a website. The application is basically a wrapper around the website, which implements things like push notifications. When a push notification is clicked, the following code is used to go to the appropriate page:
- (NSString *)handlePush:(NSDictionary *)notification
{
if ([[notification objectForKey:#"aps"] objectForKey:#"badge"]) {
int badgeNum = [[[notification objectForKey:#"aps"] objectForKey:#"badge"] integerValue];
NSLog(#"Badge: %i, got from %#", badgeNum, [[notification objectForKey:#"aps"] objectForKey:#"badge"]);
[UIApplication sharedApplication].applicationIconBadgeNumber = badgeNum;
}
if (!active) {
NSLog(#"Got noti while not active, going to that chat!");
NSString *hash;
if ((hash = [notification objectForKey:#"h"])) {
NSLog(#"Hash: %#", [hash stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]);
return [NSString stringWithFormat:#"#%#", hash];
}
return #"";
}
return nil;
}
Active is changed when the application enters the background and after it resumes, to make sure it does not trigger when a push notification arrives when the user is using the app.
The URL is parsed properly, because if i manually paste the exact same URL in the browser, i do go to the correct page.
I am 100% certain the delegate is set, as the UIWebView: ShouldStartLoadWithRequest method is called:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *url = [[request URL] absoluteString];
NSLog(#"shouldStartLoadWithRequest called with url %#", url);
if ([url hasPrefix:BASE_URL]) {
NSLog(#"Yea, i'll load this one for ya");
// remove any get-params / hash suffix
NSRange r = [url rangeOfString:#"?"];
if (r.location == NSNotFound)
r = [url rangeOfString:#"#"];
if (r.location != NSNotFound)
url = [url substringToIndex:r.location];
if (![url isEqualToString:[defaults valueForKey:#"baseUrl"]]) {
NSLog(#"setting new baseUrl %#", url);
[defaults setValue:url forKey:#"baseUrl"];
[defaults synchronize];
}
NSLog(#"Should go and load it now...");
return YES;
}
}
There's some logic for caching versions of the webpage in there. I stepped through with breakpoints, and it reaches the return YES part, and the logs in there are called. However, in that same delegate, the didStartLoad and didFailLoadWithError are not called, which contain purely NSLogs.
On the initial application start, this does work, and it has worded one time too where i stepped through for a long time using the breakpoint, so it seems it's some timing issue. Hoping not having to use any hacky solutions like timers, i was hoping that anyone around here has experience with a similar problem, or has any other valuable input.
Related
I am downloading data from a cloud API, and the API is structured so that I download several "pages" of data, for example items 1-100 and then 101-200 etc, so the flow of the code is as follows
Call API that tells us how many pages we need to download
For each page send an API call using NSURLSession
When each call returns update a counter so that we know when they are all done
When the counter reaches the answer from step 1, we send a notification to the GUI that all downloads are done and update GUI accordingly
My problem is that I am ending up in a race condition and it seems that several API calls are updating my counter at the same time thus causing it to not be correct.
Here is my code that handles the updating the counter and then sends the Notification
+ (void) fcFoundProductNumber:(NSNotification *)notification {
if([NWTillHelper isDebug] == 1) {
NSLog(#"%s entered", __PRETTY_FUNCTION__);
}
NSMutableArray *fcVariants = [[NSMutableArray alloc] init];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
int numberOfPages = [[userDefaults objectForKey:#"fcNumberOfPages"] intValue];
int loopCounter = [[userDefaults objectForKey:#"loopCounter"] intValue] + 1;
[userDefaults setObject:[NSNumber numberWithInt:loopCounter] forKey:#"loopCounter"];
[userDefaults synchronize];
if([NWTillHelper isDebug] == 1) {
NSLog(#"%s \r\nnumberOfPages = %d\r\nloopCounter = %d", __PRETTY_FUNCTION__, numberOfPages, loopCounter);
}
if(numberOfPages == loopCounter) {
if([NWTillHelper isDebug] == 1) {
NSLog(#"%s numberOfPages == loopCounter\r\nnumberOfPages = %d\r\nloopCounter = %d", __PRETTY_FUNCTION__, numberOfPages, loopCounter);
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
NSArray *fcProduct = [userDefaults objectForKey:#"fcProduct"];
[userDefaults setObject:[NSNumber numberWithInt:0] forKey:#"loopCounter"];
[userDefaults synchronize];
for(NSDictionary *fcVariant in fcProduct) {
NSMutableDictionary *fcVariantRow = [[NSMutableDictionary alloc] init];
NSArray *fcVariantSwatches = fcVariant[#"availableSwatches"];
NSDictionary *fcVariantSwatch = [fcVariantSwatches objectAtIndex:0];
NSString *activityArticleNumber = fcVariant[#"activityArticleNumber"];
NSString *colourDescription = fcVariant[#"colourDescription"];
NSString *name = fcVariant[#"name"];
NSString *price = fcVariant[#"priceInfo"][#"price"];
NSString *onSale = fcVariant[#"priceInfo"][#"onSale"];
NSString *formattedPrice = fcVariant[#"priceInfo"][#"formattedPrice"];
NSString *primaryImage = fcVariant[#"primaryImage"][#"url"];
NSString *stockState = fcVariant[#"stockState"];
NSString *variantSwatch = fcVariantSwatch[#"fabricUrl"];
[fcVariantRow setObject:activityArticleNumber forKey:#"fcVariantItemId"];
[fcVariantRow setObject:colourDescription forKey:#"fcVariantColourDescription"];
[fcVariantRow setObject:name forKey:#"fcVariantName"];
[fcVariantRow setObject:price forKey:#"fcVariantPrice"];
[fcVariantRow setObject:onSale forKey:#"fcVariantOnSale"];
[fcVariantRow setObject:formattedPrice forKey:#"fcVariantFormattedPrice"];
[fcVariantRow setObject:primaryImage forKey:#"fcVariantImageUrl"];
[fcVariantRow setObject:stockState forKey:#"fcVariantStockState"];
[fcVariantRow setObject:variantSwatch forKey:#"fcVariantSwatch"];
[fcVariants addObject:fcVariantRow];
}
[userDefaults setObject:fcVariants forKey:#"fcVariants"];
[userDefaults synchronize];
[[NSNotificationCenter defaultCenter] postNotificationName:#"fcVariantsDone" object:nil];
}
}
And here is the debug that illustrates the problem
2017-12-20 19:33:52.504312+0800 NWMPos[29258:18702451] +[FullWebServices fcFoundProductNumber:] entered
2017-12-20 19:33:52.507301+0800 NWMPos[29258:18701949] +[FullWebServices fcFoundProductNumber:] entered
2017-12-20 19:33:52.507203+0800 NWMPos[29258:18701928] +[FullWebServices fcFoundProductNumber:] entered
2017-12-20 19:33:52.509736+0800 NWMPos[29258:18702451] +[FullWebServices fcFoundProductNumber:]
numberOfPages = 3
loopCounter = 1
2017-12-20 19:33:52.512614+0800 NWMPos[29258:18701949] +[FullWebServices fcFoundProductNumber:]
numberOfPages = 3
loopCounter = 2
2017-12-20 19:33:52.512614+0800 NWMPos[29258:18701928] +[FullWebServices fcFoundProductNumber:]
numberOfPages = 3
loopCounter = 2
As you can see from the top 3 rows I am sending out 3 API calls and 3 are returning, but the last 2 seem to be returning at the same time and as you can see that causes the counter not to be updated.
How can I avoid this race condition?
That approach sounds fragile to me. Here are some suggestions for improvements (including fixing this bug):
You should not under any circumstances use NSUserDefaults for something that changes frequently. NSUserDefaults writes data to disk. You're abusing the flash storage on the device by doing that.
You should not be counting. You should be storing state in a mutable dictionary with the page number as the key, e.g.
#synchronized(self) {
self.pageDictionary[#(pageNumber)] = pageData;
}
Then self.pageDictionary.count is the number of pages retrieved to date. The #synchronize block around the set is because NSMutableDictionary isn't fully async-safe. You could wrap that around your math and it might work, but it's still a bad idea for the reasons previously mentioned.
The dictionary approach also has the benefit of letting you retry intelligently when (not if) network errors cause some of those loads to fail.
You probably shouldn't need to wait for the whole doc to be ready to make things available. If a page isn't ready, wait for it and render a blank page. You can tell if the page is ready by checking the dictionary.
If you really do need to wait for everything to be ready for some odd reason, use a dispatch group, e.g.
#implementation foo {
dispatch_group_t _syncGroup;
}
- (...)init {
_syncGroup = dispatch_group_create();
}
- (...)startTask... {
...
dispatch_group_enter(_syncGroup);
NSURLSessionDataTask *task = [NSURLSessionDataTask dataTaskWithURL:...
completionHandler:^{
dispatch_group_leave(_syncGroup);
}];
[task resume];
dispatch_group_notify(_syncGroup, dispatch_get_main_queue(),^{
[self allTasksFinished];
});
}
- (void)allTasksFinished {
// When we get here, the dispatch group went empty.
}
and when allTasksFinished is called, check and see if the count matches, then retry any failed page loads.
This method sets the background object.
- (void) downloadWithURL: (NSMutableArray *)urlArray
pathArr: (NSMutableArray *)pathArr
mediaInfo: (MediaInfo *)mInfo
{
bgDownloadMediaInfo = mInfo;
reqUrlCount = urlArray.count;
dict = [NSDictionary dictionaryWithObjects:pathArr
forKeys:urlArray];
mutableDictionary = [dict mutableCopy];
backgroundConfigurationObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"XXXXX"];
backgroundConfigurationObject.sessionSendsLaunchEvents = YES;
backgroundConfigurationObject.discretionary = YES;
backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigurationObject
delegate: self delegateQueue: [NSOperationQueue currentQueue]];
self.requestUrl = [urlArray objectAtIndex:0];
download = [backgroundSession downloadTaskWithURL:self.requestUrl];
[download resume];
}
These are the completion handlers.
#pragma Mark - NSURLSessionDownloadDelegate
- (void)URLSession: (NSURLSession *)session
downloadTask: (NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL: (NSURL *)location
{
LogDebug(#"Download complete for request url (%#)", downloadTask.currentRequest.URL);
NSString *temp = [mutableDictionary objectForKey:downloadTask.currentRequest.URL];
NSString *localPath = [NSString stringWithFormat: #"%#",temp];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *destinationURL = [NSURL fileURLWithPath: localPath];
NSError *error = nil;
[fileManager moveItemAtURL:location toURL:destinationURL error:&error];
LogDebug(#"Moving download file at url : (%#) to : (%#)", downloadTask.currentRequest.URL, destinationURL);
reqUrlCount --;
downloadSegment ++;
// Handover remaining download requests to the OS
if ([finalUrlArr count] != 0) {
// remove the request from the array that got downloaded.
[finalUrlArr removeObjectAtIndex:0];
[finalPathArr removeObjectAtIndex:0];
if ([finalUrlArr count] > 0) {
// proceed with the next request on top.
self.requestUrl = [finalUrlArr objectAtIndex:0];
download = [backgroundSession downloadTaskWithURL:self.requestUrl];
[download resume];
}
}
if ([adsArray count] > 0) {
adsArrayCount --;
// delegate back once all the ADs segments have been downloaded.
if (adsArrayCount == 0) {
for (int i = 0; i < [adsArray count]; i++) {
NSArray *ads = [adsArray objectAtIndex: i];
for (int j = 0; j < [ads count]; j++) {
MediaInfo *ad = [ads objectAtIndex: j];
[self setDownloadComplete: ad];
// skip sending downloadFinish delegate if the media is marked as downloadDone
if (!ad.downloadDone) {
[delegate MediaDownloadDidFinish: ad.mediaId error: NO];
}
ad.downloadDone = YES;
}
}
downloadSegment = 0;
}
}
// delegate back once all the main media segments have been downloaded.
if (reqUrlCount == 0) {
[self setDownloadComplete: mediaInfo];
state = DownloadState_Done;
// skip sending downloadFinish delegate if the media is marked as downloadDone
if (!bgDownloadMediaInfo.downloadDone) {
[delegate MediaDownloadDidFinish: bgDownloadMediaInfo.mediaId error: NO];
}
bgDownloadMediaInfo.downloadDone = YES;
[urlArr release];
[pathArr release];
[finalUrlArr release];
[finalPathArr release];
// invalidate the NSURL session once complete
[backgroundSession invalidateAndCancel];
}
}
- (void)URLSession: (NSURLSession *)session
task: (NSURLSessionTask *)task
didCompleteWithError: (NSError *)error
{
if (error) {
NSLog(#"Failure to download request url (%#) with error (%#)", task.originalRequest.URL, error);
}
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
// save the total downloaded size
[self downloaderDidReceiveData:bytesWritten];
// enable the log only for debugging purpose.
// LogDebug(#"totalBytesExpectedToWrite %llu, totalBytesWritten %llu, %#", totalBytesExpectedToWrite, totalBytesWritten, downloadTask.currentRequest.URL);
}
With out this code(beginBackgroundTaskWithExpirationHandler) the download stops when the app is pushed into background.
// AppDelegate_Phone.m
- (void)applicationDidEnterBackground: (UIApplication *)application
{
NSLog(#"applicationDidEnterBackground");
UIApplication *app = [UIApplication sharedApplication];
UIBackgroundTaskIdentifier bgTask;
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
}];
}
Have you implemented application:handleEventsForBackgroundURLSession:completionHandler: in your app delegate? That should save the completion handler and start background session with the specified identifier.
If you don't implement that method, your app will not be informed if the download finishes after the app has been suspended (or subsequently terminated in the course of normal app lifecycle). As a result, it might look like the download didn't finish, even though it did.
(As an aside, note that if the user force-quits the app, that not only terminates the download, but obviously won't inform your app that the download was terminated until the user manually restarts the app at some later point and your app re-instantiates the background session. This is a second-order concern that you might not worry about until you get the main background processing working, but it's something to be aware of.)
Also, your URLSessionDidFinishEventsForBackgroundURLSession: must call that saved completion handler (and dispatch this to the main queue).
Also, your design looks like it will issue only one request at a time. (I'd advise against that, but let's just assume it is as you've outlined above.) So, let's imagine that you have issued the first request and the app is suspended before it's done. Then, when the download is done, the app is restarted in the background and handleEventsForBackgroundURLSession is called. Let's assume you fixed that to make sure it restarts the background session so that the various delegate methods can be called. Make sure that when you issue that second request for the second download that you use the existing background session, not instantiating a new one. You can have only one background session per identifier. Bottom line, the instantiation of the background session should be decoupled from downloadWithURL:pathArr:mediaInfo:. Only instantiate a background session once.
Add "Required background modes" in your .plist
There, add the item "App downloads content from the network"
I must say I feel like an idiot right now. :) I've been up and down Facebook, Google and StackOverflow and still cannot get the answer to what I am doing wrong! :) I've looked at both Facebook examples: Hackbook and WishList. Wishlist is obviously the one that should tell me what to do, but ALL the examples I've seen have the OBJECT part as a URL. I do not necessarily desire this, as I just want the post to say (this user) is playing [MyGame].
Ok here's my goal. I have an iPhone game. I want to do what Spotify does when you listen to a song, which posts to timeline and ticker. I'd also like to use this to post the player's score on the user's timeline and ticker.
I setup Open Graph with an action called Play and an object called Game along with it's aggregator. I think I also will need an action called Score?
Anyways, I can successfully post to a user's wall using feed dialog, but that's not what I want for the Play action.
Here's a concise version of what I got so far, any help is much appreciated:
Couple Notes:
I have a singleton FacebookInfo which takes care of handling Facebook delegates and stuff. I also have a FacebookUser class which hold the current user's info for the current session, populated when calling me. I also have a DLog method which simply does an NSlog only on debug mode.
When a user clicks the Play button in my game, I would like to call my method [[Facebook sharedInfo] publishAction:#"play"] below. I'm passing an NSString as action so later I can call the same method and use an action like Score and just modify the post accordingly.
#interface FacebookInfo : NSObject {
Facebook *_facebook;
FacebookUser *_facebookUser;
}
#property (nonatomic, retain) Facebook *facebook;
#property (nonatomic, retain) FacebookUser *facebookUser;
+(id)sharedInfo;
-(BOOL)isFacebookAuthenticated;
-(void)fbDidLogout;
-(void)getMe;
-(void)publishFeed;
-(void)publishWithAction:(NSString *)action;
#end
static FacebookInfo *facebookInfo = nil;
#implementation FacebookInfo
#synthesize facebook = _facebook;
#synthesize facebookUser = _facebookUser;
#pragma mark - Custom Methods
-(void)getMe {
DLog(#"**********");
/* when forcing FBConnect to show inline dialog instead of using SSO, this works.
apparently this fails when using SSO, error:
Err message: (null)
Err code: 10000
*/
[[self facebook] requestWithGraphPath:#"me" andDelegate:self];
}
-(void)publishWithAction:(NSString *)action {
DLog(#"**********");
if ([action isEqualToString:#"play"]) {
// Build the params list
NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithCapacity:1];
// all sample have this pointing to a URL. Do i really need to do that?
[params setValue:kFBAppNameSpace forKey:#"game"];
// I know I may need more parameters, but which are required?
// Do I need to add the one's that Facebook Javascript examples have,
// like title, description? I think it's here where I'm mostly confused.
// Make the Graph API call to add to the wishlist
[[self facebook] requestWithGraphPath:#"me/[myappnamespace]:play"
andParams:params
andHttpMethod:#"POST"
andDelegate:self];
[params release];
}
}
-(void)publishFeed {
DLog(#"**********");
/*
This works perfectly fine
*/
SBJSON *jsonWriter = [[SBJSON new] autorelease];
NSDictionary *actionLinks = [NSArray arrayWithObjects:
[NSDictionary dictionaryWithObjectsAndKeys:
#"Get The App",#"name",
kFBAppURL,#"link",
nil],
nil];
NSString *actionLinksStr = [jsonWriter stringWithObject:actionLinks];
NSString *app_id = kFBAppID;
NSString *user_message_prompt = [NSString stringWithFormat:#"Post to Your Wall!"];
NSString *name = [NSString stringWithFormat:#"[MyGameName]"];
NSString *caption = [NSString stringWithFormat:#"%# has gotten a score of %#!",[[self facebookUser] firstName],[[[GameInfo sharedGameInfo] scoreTotal] stringValue]];
NSString *description = [NSString stringWithFormat:#"Can you beat this score?!"];
NSString *link = kFBAppURL;
NSString *picture = kFBAppImage;
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
app_id, #"app_id",
user_message_prompt, #"user_message_prompt",
name, #"name",
caption, #"caption",
description, #"description",
link, #"link",
picture, #"picture",
actionLinksStr, #"actions",
nil];
[[self facebook] dialog:#"feed"
andParams:params
andDelegate:self];
}
-(BOOL)checkForPreviousAccessToken {
DLog(#"**********");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:#"FBAccessTokenKey"] && [defaults objectForKey:#"FBExpirationDateKey"]) {
DLog(#"FB: Token Exists!");
[[self facebook] setAccessToken:[defaults objectForKey:#"FBAccessTokenKey"]];
[[self facebook] setExpirationDate:[defaults objectForKey:#"FBExpirationDateKey"]];
}
if (![[self facebook] isSessionValid]) {
DLog(#"FB: Authorizing...");
NSArray *permissions = [[NSArray alloc] initWithObjects:
#"publish_stream",
#"publish_actions",
#"offline_access",
nil];
[[self facebook] authorize:permissions];
[permissions release];
} else {
DLog(#"FB: Authorized!!!");
// show logged in
[self getMe];
}
return [[self facebook] isSessionValid];
}
-(BOOL)isFacebookAuthenticated {
DLog(#"**********");
return [self checkForPreviousAccessToken];
}
-(void)extendAccessTokenIfNeeded {
DLog(#"**********");
[[self facebook] extendAccessTokenIfNeeded];
[[FacebookInfo sharedInfo] getMe];
}
-(void)logout {
DLog(#"**********");
[[self facebook] logout:self];
}
#pragma mark - FBConnect Delegate Methods
-(void)fbDidLogin {
DLog(#"**********");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[[self facebook] accessToken] forKey:#"FBAccessTokenKey"];
[defaults setObject:[[self facebook] expirationDate] forKey:#"FBExpirationDateKey"];
[defaults synchronize];
[self getMe];
}
- (void)fbDidNotLogin:(BOOL)cancelled {
DLog(#"**********");
}
- (void)fbDidExtendToken:(NSString*)accessToken expiresAt:(NSDate*)expiresAt {
DLog(#"**********");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:accessToken forKey:#"FBAccessTokenKey"];
[defaults setObject:expiresAt forKey:#"FBExpirationDateKey"];
[defaults synchronize];
}
-(void)fbDidLogout {
DLog(#"**********");
// Remove saved authorization information if it exists
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:#"FBAccessTokenKey"]) {
[defaults removeObjectForKey:#"FBAccessTokenKey"];
[defaults removeObjectForKey:#"FBExpirationDateKey"];
[defaults synchronize];
}
}
- (void)fbSessionInvalidated {
DLog(#"**********");
}
#pragma mark - FBRequestDelegate Methods
/**
* Called when the Facebook API request has returned a response. This callback
* gives you access to the raw response. It's called before
* (void)request:(FBRequest *)request didLoad:(id)result,
* which is passed the parsed response object.
*/
- (void)request:(FBRequest *)request didReceiveResponse:(NSURLResponse *)response {
DLog(#"**********");
//DLog(#"received response");
}
/**
* Called when a request returns and its response has been parsed into
* an object. The resulting object may be a dictionary, an array, a string,
* or a number, depending on the format of the API response. If you need access
* to the raw response, use:
*
* (void)request:(FBRequest *)request
* didReceiveResponse:(NSURLResponse *)response
*/
- (void)request:(FBRequest *)request didLoad:(id)result {
DLog(#"**********");
//code removed for this example
}
/**
* Called when an error prevents the Facebook API request from completing
* successfully.
*/
- (void)request:(FBRequest *)request didFailWithError:(NSError *)error {
DLog(#"**********");
DLog(#"Err message: %#", [[error userInfo] objectForKey:#"error_msg"]);
DLog(#"Err code: %d", [error code]);
if ([error code] == 190) {
// logout
} else {
DLog(#"There was an error making your request.");
}
}
#pragma mark - Singleton Methods
+ (id)sharedInfo {
//DLog(#"**********");
#synchronized(self) {
if(facebookInfo == nil)
facebookInfo = [[super allocWithZone:NULL] init];
}
return facebookInfo;
}
+ (id)allocWithZone:(NSZone *)zone {
DLog(#"**********");
return [[self sharedInfo] retain];
}
- (id)copyWithZone:(NSZone *)zone {
DLog(#"**********");
return self;
}
- (id)retain {
DLog(#"**********");
return self;
}
- (unsigned)retainCount {
DLog(#"**********");
return UINT_MAX; //denotes an object that cannot be released
}
- (oneway void)release {
DLog(#"**********");
// never release
}
- (id)autorelease {
DLog(#"**********");
return self;
}
- (id)init {
DLog(#"**********");
if ((self = [super init]) != NULL) {
//Init
[self setFacebook:[[[Facebook alloc] initWithAppId:kFBAppID urlSchemeSuffix:kFBUrlSchemeSuffix andDelegate:self] autorelease]];
[self setFacebookUser:[[[FacebookUser alloc] init] autorelease]];
}
return self;
}
- (void)dealloc {
DLog(#"**********");
// Should never be called, but just here for clarity really.
DLog(#"Release FacebookInfo...");
[super dealloc];
}
#end
#implementation AppDelegate_iPhone
// Add for Facebook SSO support (4.2+)
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
[[[FacebookInfo sharedInfo] facebook] handleOpenURL:url];
}
// Add for Facebook SSO support (pre 4.2)
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
[[[FacebookInfo sharedInfo] facebook] handleOpenURL:url];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
DLog(#"**********");
[[FacebookInfo sharedInfo] extendAccessTokenIfNeeded];
}
Wow, I know this is a long post, but I hope someone can help point me in the right direction.
UPDATE 1: (2012/18/02)
Ok. So I settled for making a page which contains the og meta-data which I did not want to do and supplied the url for my object. As per the FB Documentation, found here:
Open Graph Mechanics
When users take an action in your app, such as cook the Stuffed Cookie, the app calls a Graph API to create a new cook action that connects the user with the Stuffed Cookie object. This is accomplished by issuing a HTTP POST to the user’s /me/myapp:cook connection with the URL of the recipe object. Facebook will then crawl the object URL, read the metadata, and connect the object to user's Graph via the action.
The diagram below illustrates the process:
User takes an action in the app, such as "cook" a "recipe"
App calls a Graph API /me/action:object=Object_URL
Facebook will crawl the object URL, read its meta tags and connects the object to the user's Graph via the action.
It would be cool if we can define these things in the app itself as params for cases where I do not need a website for.
As of 4/21/12 Facebook requires you to have created a page to get the data desired.
How can I change the user-agent of UIWebView in iOS 5?
What I have done so far:
Using the delegate call back, intercept the NSURLRequest, create a new url request and set it's user-agent as whatever I want, then download the data and reload the UIWebView with "loadData:MIMEType:....".
Problem:
This causes infinite recursion, where I load the data, which calls the delegate back, which intern calls the delegate....
Here's the delegate method:
- (BOOL)webView:(UIWebView *)aWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
dispatch_async(kBgQueue, ^{
NSURLResponse *response = nil;
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:[request URL]];
NSDictionary *headers = [NSDictionary dictionaryWithObject:
#"custom_test_agent" forKey:#"User-Agent"];
[newRequest setAllHTTPHeaderFields:headers];
[self setCurrentReqest:newRequest];
NSData *data = [NSURLConnection sendSynchronousRequest:newRequest
returningResponse:&response
error:nil];
dispatch_sync(dispatch_get_main_queue(), ^{
[webView loadData:data
MIMEType:[response MIMEType]
textEncodingName:[response textEncodingName]
baseURL:[request URL]];
});
});
return YES;
}
Change the "UserAgent" default value by running this code once when your app starts:
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:#"Your user agent", #"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
EDIT: I have used this with great success, but want to add additional details. To get a user agent, you can enable the "Developer" menu, set the user agent, and then connect to this site to get it printed out for you: WhatsMyAgent. Likewise you can connect using any kind of mobile device, and get it that way too. BTW this is still working just fine in iOS7+
In Swift use this to set UserAgent,
func setUserAgent(){
var userAgent = NSDictionary(objectsAndKeys: "YourUserAgentName","UserAgent")
NSUserDefaults.standardUserDefaults().registerDefaults(userAgent as [NSObject : AnyObject])
}
Use this to test,
println(WebView.stringByEvaluatingJavaScriptFromString("navigator.userAgent"));
When you send message [aWebView loadData:MIMEType:textEncodingName:baseURL:]
then aWebView shouldStartLoadWithRequest: will be called again, and then again - that is why you get an infinite recursion
You should restrict calling of your dispatch_async() block, for example by using some conventional URL:
- (BOOL)webView:(UIWebView *)aWebView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
if ([[[request URL] absoluteString] isEqualToString:#"http://yourdomain.com/?local=true"]) {
return YES;
}
...
dispatch_async(...
[aWebView loadData:data
MIMEType:[response MIMEType]
textEncodingName:[response textEncodingName]
baseURL:[NSURL URLWithString:#"http://yourdomain.com/?local=true"]];
);
return NO;
}
when my app starts music is playing:
-(void)playBgMusic {
NSString *path = [[NSBundle mainBundle] pathForResource:#"bgmusic" ofType:#"aif"];
theAudio = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:NULL];
theAudio.delegate = self;
[theAudio play]; }
but he should be able to turn the music off by pressing a button if he presses the button again the music should turn on again. i have:
-(IBAction)check {
if (isquiet == NO) {
[theAudio stop];
isquiet = YES;
defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:#"stringKey"];
}
else {
[self playBgMusic];
isquiet = NO;
defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:NO forKey:#"stringKey"]; } }
I think I didn't get it. Now it works in my first ViewController that I can turn the music on and off but when I go to another viewController while the music is playing, then back again and press the button, the music doesn't stop and when i press it many times the music is played a second time and overlaps.
What's still wrong?
No need to wrap it in an NSNumber, there are some convenience methods for this:
To set a BOOL, use:
[userDefaults setBool:YESorNO forKey:#"yourKey"];
To access it, use:
[userDefaults boolForKey:#"yourKey"];
[EDIT TO ANSWER YOUR ADDITIONAL QUESTION]
Not sure why you are using NSUserDefaults - it seems unnecessary for what you are trying to achieve? Here's what I would do for a button that can start/stop music:
-(IBAction)check
{
if (isQuiet)
{
// Play music
// Change the button to indicate it is playing...
} else
{
// Stop music
// Change the button to indicate it has stopped...
}
// Set your isQuiet to be the opposite of what it was when the button was clicked
isQuiet = !isQuiet;
}
Box your BOOL value to NSNumber object and add it to NSUserDefault:
NSUserDefaults *boolUserDefaults = [NSUserDefaults standardUserDefaults];
[boolUserDefaults setObject:[NSNumber numberWithBool:isquiet]
forKey:#"stringKey"];
Later you'll be able to retrieve that value as plain BOOL using -boolForKey: function in NSUserDefaults
To save:
[boolUserDefaults setObject:[NSNUmber numberWithBool:isQuiet] forKey:#"stringKey"];
When you read it back, read it as a NSNumber then do:
BOOL savedIsQuiet = [theNumberYouSaved boolValue];
Swift:
To save bool:
UserDefaults.standard.set(true, forKey: "storageKey")
To retrieve the bool:
let boolValue = UserDefaults.standard.bool(forKey: "storageKey")