AVAudioPlayer for Sprite Kit - objective-c

I'm utilizing AVAudioPlayer in MyScene - the main game scene in my Sprite Kit game. I also have an OptionsScene containing 2 buttons. 1 of the buttons is for switching music ON/OFF.
Everything works except pressing the button doesn't instantly turn music ON or OFF. I have to leave the app, double-tap iPhone's Home button & swipe-up the game View to turn it off completely.
Once that is done, I can open the game & the previously selected ON/OFF button takes effect. Is there a way to have AVAudioPlayer turn off instantly upon clicking it's button in the options scene?
I do use a custom class for playing the music that I did not write. So I don't completely understand how it works. Here is the method I use inside MyScene to activate the AVAudioPlayer:
-(void)musicPlayer
{
defaults = [NSUserDefaults standardUserDefaults]; //"Activate" defaults.
music = [defaults boolForKey:#"MusicButtonKEY"]; //Then assign that defaults to a BOOL (music in this case).
if (music == YES) //Check what defaults BOOL key value was set to & act accordingly to that.
{
[self.audioController tryPlayMusic]; //tryPlayMusic is a method from AudioController class.
NSLog(#"Music playing");
}
else /*if (music == NO)*/
{
[self.audioController stopPlayingMusic]; //stopPlayingMusic is a method from AudioController class.
NSLog(#"Music not playing");
}
}
Here is the .h & .m files of the AudioController class:
AudioController.h
#import <Foundation/Foundation.h>
#interface AudioController : NSObject
-(instancetype)init;
-(void)tryPlayMusic;
-(void)stopPlayingMusic;
#end
AudioController.m
#import "AudioController.h"
#import AVFoundation;
#interface AudioController () <AVAudioPlayerDelegate>
#property (strong, nonatomic) AVAudioSession *audioSession;
#property (strong, nonatomic) AVAudioPlayer *backgroundMusicPlayer;
#property (assign) BOOL backgroundMusicPlaying;
#property (assign) BOOL backgroundMusicInterrupted;
#end
#implementation AudioController
#pragma mark - Public
-(instancetype)init
{
self = [super init];
if (self)
{
[self configureAudioSession];
[self configureAudioPlayer];
}
return self;
}
-(void)tryPlayMusic
{
//If background music or other music is already playing, nothing more to do here
if (self.backgroundMusicPlaying || [self.audioSession isOtherAudioPlaying])
{
return;
}
// Play background music if no other music is playing and we aren't playing already
//Note: prepareToPlay preloads the music file and can help avoid latency. If you don't
//call it, then it is called anyway implicitly as a result of [self.backgroundMusicPlayer play];
//It can be worthwhile to call prepareToPlay as soon as possible so as to avoid needless
//delay when playing a sound later on.
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];
self.backgroundMusicPlaying = YES;
}
-(void)stopPlayingMusic
{
if (self.backgroundMusicPlaying == YES)
{
[self.backgroundMusicPlayer stop];
self.backgroundMusicPlaying = NO;
}
}
#pragma mark - Private
-(void)configureAudioSession
{
//Implicit initialization of audio session.
self.audioSession = [AVAudioSession sharedInstance];
//Set category of audio session
//See handy chart on pg. 46 of the Audio Session Programming Guide for what the categories mean
//Not absolutely required in this example, but good to get into the habit of doing
//See pg. 10 of Audio Session Programming Guide for "Why a Default Session Usually Isn't What You Want"
NSError *setCategoryError = nil;
if ([self.audioSession isOtherAudioPlaying])
{ //Mix sound effects with music already playing
[self.audioSession setCategory:AVAudioSessionCategorySoloAmbient error:&setCategoryError];
self.backgroundMusicPlaying = NO;
}
else
{
[self.audioSession setCategory:AVAudioSessionCategoryAmbient error:&setCategoryError];
}
if (setCategoryError)
{
NSLog(#"Error setting category! %ld", (long)[setCategoryError code]);
}
}
-(void)configureAudioPlayer
{
//Create audio player with background music.
NSString *backgroundMusicPath = [[NSBundle mainBundle] pathForResource:#"ThemeA" ofType:#"mp3"];
NSURL *backgroundMusicURL = [NSURL fileURLWithPath:backgroundMusicPath];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:nil];
self.backgroundMusicPlayer.delegate = self; //We need this so we can restart after interruptions
self.backgroundMusicPlayer.numberOfLoops = -1; //Negative number means loop forever
}
#pragma mark - AVAudioPlayerDelegate methods
-(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player
{
//It is often not necessary to implement this method since by the time
//this method is called, the sound has already stopped. You don't need to
//stop it yourself.
//In this case the backgroundMusicPlaying flag could be used in any
//other portion of the code that needs to know if your music is playing.
self.backgroundMusicInterrupted = YES;
self.backgroundMusicPlaying = NO;
}
-(void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flag
{
//Since this method is only called if music was previously interrupted, you know that the music has stopped playing & can now be resumed.
[self tryPlayMusic];
self.backgroundMusicInterrupted = NO;
}
#end
Here is the button code from OptionsScene:
#import "OptionsScene.h"
#import "ViewController.h"
BOOL soundON;
BOOL musicON;
#interface OptionsScene ()
#property (strong, nonatomic) AGSpriteButton *AGSoundButton;
#property (strong, nonatomic) AGSpriteButton *AGMusicButton;
#end
#implementation OptionsScene
{
NSUserDefaults *defaults;
}
-(id)initWithSize:(CGSize)size
{
self = [super initWithSize:size];
{
defaults = [NSUserDefaults standardUserDefaults];
[self background]; //Won't show code for this method since it just loads the background.
//Background music & sound dispatched once to get things going in this scene so to speak.
static dispatch_once_t onceOS;
dispatch_once(&onceOS, ^ {
//Code to run once
soundON = YES;
musicON = YES;
[defaults setBool:YES forKey:#"SoundButtonKEY"];
[defaults synchronize];
self.AGSoundButton.texture = [SKTexture textureWithImageNamed:#"checkboxYES"];
NSLog(#"SOUND switched to ON");
[defaults setBool:YES forKey:#"MusicButtonKEY"];
[defaults synchronize];
self.AGMusicButton.texture = [SKTexture textureWithImageNamed:#"checkboxYES"];
NSLog(#"MUSIC switched to ON");
});
}
return self;
}
-(void)update:(NSTimeInterval)currentTime
{
}
-(void)didMoveToView:(SKView *)view
{
//Sound button.
self.AGSoundButton = [AGSpriteButton buttonWithImageNamed:#"checkboxYES"];
self.AGSoundButton.position = CGPointMake(self.size.width/2 + 50, self.size.height/2 + 70);
self.AGSoundButton.zPosition = 2.2;
[self.AGSoundButton addTarget:self selector:#selector(soundInit) withObject:[NSValue valueWithCGPoint:CGPointMake(self.size.width + 230, self.size.height/2 + 3)] forControlEvent:AGButtonControlEventTouchUpInside];
[self addChild:self.AGSoundButton];
//Music button.
self.AGMusicButton = [AGSpriteButton buttonWithImageNamed:#"checkboxYES"];
self.AGMusicButton.position = CGPointMake(self.size.width/2 + 50, self.size.height/2 + 10);
self.AGMusicButton.zPosition = 2.2;
[self.AGMusicButton addTarget:self selector:#selector(musicInit) withObject:[NSValue valueWithCGPoint:CGPointMake(self.size.width + 230, self.size.height/2 + 3)] forControlEvent:AGButtonControlEventTouchUpInside];
[self addChild:self.AGMusicButton];
//Call upon the saved BOOL/Button positions from their respective key's.
BOOL soundDefaults = [defaults boolForKey:#"SoundButtonKEY"];
BOOL musicDefaults = [defaults boolForKey:#"MusicButtonKEY"];
//Sound button retrieval.
if (soundDefaults == YES)
{
self.AGSoundButton.texture = [SKTexture textureWithImageNamed:#"checkboxYES"];
NSLog(#"Sound is ON");
}
if (soundDefaults == NO)
{
self.AGSoundButton.texture = [SKTexture textureWithImageNamed:#"checkboxNO"];
NSLog(#"Sound is OFF");
}
//Music button retrieval.
if (musicDefaults == YES)
{
self.AGMusicButton.texture = [SKTexture textureWithImageNamed:#"checkboxYES"];
NSLog(#"Music is ON");
}
if (musicDefaults == NO)
{
self.AGMusicButton.texture = [SKTexture textureWithImageNamed:#"checkboxNO"];
NSLog(#"Music is OFF");
}
}
//Sound button switch ON/OFF.
-(void)soundInit
{
if (soundON == YES)
{
soundON = NO;
[defaults setBool:NO forKey:#"SoundButtonKEY"];
[defaults synchronize];
self.AGSoundButton.texture = [SKTexture textureWithImageNamed:#"checkboxNO"];
NSLog(#"SOUND switched to OFF");
}
else /*if (soundON == NO)*/
{
soundON = YES;
[defaults setBool:YES forKey:#"SoundButtonKEY"];
[defaults synchronize];
self.AGSoundButton.texture = [SKTexture textureWithImageNamed:#"checkboxYES"];
NSLog(#"SOUND switched to ON");
}
}
//Music button switch ON/OFF.
-(void)musicInit
{
if (musicON == YES)
{
musicON = NO;
[defaults setBool:NO forKey:#"MusicButtonKEY"];
[defaults synchronize];
self.AGMusicButton.texture = [SKTexture textureWithImageNamed:#"checkboxNO"];
NSLog(#"MUSIC switched to OFF");
}
else /*if (musicON == NO)*/
{
musicON = YES;
[defaults setBool:YES forKey:#"MusicButtonKEY"];
[defaults synchronize];
self.AGMusicButton.texture = [SKTexture textureWithImageNamed:#"checkboxYES"];
NSLog(#"MUSIC switched to ON");
}
}
//Brings back to GameScene. Called by a 3rd button that I didn't bother including in this code. It doesn't really matter.
-(void)backToGame
{
NSLog(#"Going back");
[defaults synchronize]; //Save button states, just in case, before leaving scene.
//Brings back to game scene.
MyScene *firstScene = [MyScene sceneWithSize:self.size];
[self.view presentScene:firstScene];
}
#end
Really hope this helps with the details.

Related

Xcode iOS app hanging on launch screen "semaphore_wait_trap()"

Excuse me but I'm a total Noob, not a programmer. I based a photo editing app on a template and customised it heavily with help from Google searches, tutorials etc.
Using Xcode 7.3.1, iOS 9.3, newer Photosframework and only objective C.
Ive got the app to a point that Im happy with it, except that I noticed on first launch, the app hangs (debug reports semaphore_wait_trap().
The app can't get to next step "request to access photos" alert pop up in iOS 9.3, and only way to get to it is to hit the home button, then see the grant access alert, then switch back to app. Then quit the app, reload it and then it runs fine overtime after that. This is of course not an ideal user experience.
I see if I pause on debug mode its hanging on: "semaphore_wait_trap()"
Ive googled and searched for days and can't find a solution to get the permissions alert popup to show on top of my app window.
Its beyond me. Any Ideas would be greatly appreciated.
See screen shot of the launch image that remains on top of the alert pop up.
If you press the "Home" button, the alert to grant access to photos appears.
The app delegate:
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if ([UIApplication instancesRespondToSelector:#selector(registerUserNotificationSettings:)]){
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
}
UILocalNotification *locationNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
if (locationNotification) {
// Sets icon badge number to zero
application.applicationIconBadgeNumber = 0;
}
// END Local Notification ==========================
return true;
}
-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
// Resets icon's badge number to zero
application.applicationIconBadgeNumber = 0;
}
Here is a snippet of the main View controller (hope its not to long, not sure where the problem lies)
HomeVC.m:
#import "HomeVC.h"
#import "Configs.h"
#import "AAPLGridViewCell2.h"
#import "NSIndexSet+Convenience.h"
#import "UICollectionView+Convenience.h"
#import "AAPLRootListViewController.h"
#import "Configs.h"
#import "ImageEditorTheme.h"
#import "ImageEditorTheme+Private.h"
#import PhotosUI;
#import UIKit;
#interface HomeVC()
<
PHPhotoLibraryChangeObserver,
UICollectionViewDelegateFlowLayout,
UICollectionViewDataSource,
UICollectionViewDelegate
>
#property (nonatomic, strong) NSArray *sectionFetchResults;
#property (nonatomic, strong) NSArray *sectionLocalizedTitles;
#property (nonatomic, strong) PHCachingImageManager *imageManager;
#property CGRect previousPreheatRect;
#property (nonatomic, strong) IBOutlet UICollectionViewFlowLayout *flowLayout;
#property (nonatomic, assign) CGSize lastTargetSize;
#end
#implementation HomeVC
{
UIActivityIndicatorView *_indicatorView;
}
static NSString * const AllPhotosReuseIdentifier = #"AllPhotosCell";
static NSString * const CollectionCellReuseIdentifier = #"CollectionCell";
static NSString * const CellReuseIdentifier = #"Cell";
static CGSize AssetGridThumbnailSize;
- (void)awakeFromNib {
self.imageManager = [[PHCachingImageManager alloc] init];
[self resetCachedAssets];
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
}
- (void)dealloc {
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
_logoImage.layer.cornerRadius = 30;
[self loadPhotos];
[_libraryOutlet addTarget:self action:#selector(touchUp:) forControlEvents:UIControlEventTouchUpInside];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handle_data) name:#"reload_data" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(hideMenu) name:#"hide_menu" object:nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Begin caching assets in and around collection view's visible rect.
[self updateCachedAssets];
}
-(void)handle_data {
//[self.collectionView2 layoutIfNeeded];
//[self resetCachedAssets];
[self.collectionView2 reloadData];
[self updateCachedAssets];
NSLog(#"did it work?");
}
- (void)viewDidLayoutSubviews
{
NSInteger section = [self.collectionView2 numberOfSections] - 1;
NSInteger item = [self.collectionView2 numberOfItemsInSection:section] - 1;
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
[self.collectionView2 scrollToItemAtIndexPath:indexPath atScrollPosition:(UICollectionViewScrollPositionTop) animated:NO];
//[self loadPhotos];
}
-(void) loadPhotos {
PHFetchOptions *allPhotosOptions = [[PHFetchOptions alloc] init];
allPhotosOptions.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"creationDate" ascending:YES]];
PHFetchResult *allPhotos = [PHAsset fetchAssetsWithOptions:allPhotosOptions];
if (self.assetsFetchResults == nil) {
self.assetsFetchResults = allPhotos;
}
}
#pragma mark - PHPhotoLibraryChangeObserver
- (void)photoLibraryDidChange:(PHChange *)changeInstance {
// Check if there are changes to the assets we are showing.
PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
if (collectionChanges == nil) {
return;
}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue(), ^{
// Get the new fetch result.
self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];
UICollectionView *collectionView = self.collectionView;
if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) {
// Reload the collection view if the incremental diffs are not available
[collectionView reloadData];
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
[collectionView performBatchUpdates:^{
NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
if ([removedIndexes count] > 0) {
[collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
if ([insertedIndexes count] > 0) {
[collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
if ([changedIndexes count] > 0) {
[collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
} completion:NULL];
}
[self resetCachedAssets];
});
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.assetsFetchResults.count;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; {
CGFloat colum = 3.0, spacing = 0.0;
CGFloat value = floorf((CGRectGetWidth(self.view.bounds) - (colum - 1) * spacing) / colum);
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(value, value);
layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0);
layout.minimumInteritemSpacing = spacing;
layout.minimumLineSpacing = spacing;
return CGSizeMake(value, value);
//return self.collectionView.frame.size;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
PHAsset *asset = self.assetsFetchResults[indexPath.item];
// Dequeue an AAPLGridViewCell.
AAPLGridViewCell2 *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellReuseIdentifier forIndexPath:indexPath];
cell.representedAssetIdentifier = asset.localIdentifier;
// Request an image for the asset from the PHCachingImageManager.
[self.imageManager requestImageForAsset:asset
targetSize:CGSizeMake(130, 130)
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// Set the cell's thumbnail image if it's still showing the same asset.
if ([cell.representedAssetIdentifier isEqualToString:asset.localIdentifier]) {
cell.thumbnailImage = result;
}
}];
CGPoint bottomOffset = CGPointMake(-0, self.collectionView.contentSize.height - self.collectionView.bounds.size.height + self.collectionView.contentInset.bottom);
[self.collectionView setContentOffset:bottomOffset animated:NO];;
return cell;
}
- (void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
// Prepare the options to pass when fetching the live photo.
PHAsset *asset = self.assetsFetchResults[indexPath.item];
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
options.networkAccessAllowed = NO;
dispatch_async(dispatch_get_main_queue(), ^{
_indicatorView = [ImageEditorTheme indicatorView];
_indicatorView.center = self.containerView.center;
[self.containerView addSubview:_indicatorView];
[_indicatorView startAnimating];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
PreviewVC *prevVC = (PreviewVC *)[storyboard instantiateViewControllerWithIdentifier:#"PreviewVC"];
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFit options:options resultHandler:^(UIImage *result, NSDictionary *info) {
// Show the UIImageView and use it to display the requested image.
passedImage = result;
prevVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:prevVC animated:true completion:nil];
[_indicatorView stopAnimating];
}];
});
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// Update cached assets for the new visible area.
[self updateCachedAssets];
}
I managed to solve the issue. It was as simple as removing the call to "[self resetCachedAssets];" in "awakeFromNib"
Works great now.

watch os 2 not waking parent app and changing UITableView of parent

I have a watch app that is being updated for watch os 2. The sendmessage does not wake the parent app. According to the transition documentation is this how you would wake a parent in the background.
"The iOS app is always considered reachable, and calling this method from your Watch app wakes up the iOS app in the background as needed."
Has anyone had this problem? The only way to get data is to have the parent app already open.
Another weird thing is the watch app changes the uitableview for the parent app. When the -(IBAction)yesterdaySales:(id)sender is called on the watch, it changes the parent app UITableView instead of the watch tableview.
InterfaceController.m
#import "InterfaceController.h"
#import "MyRowController.h"
#import "ftDateParser.h"
#import WatchKit;
#import <WatchConnectivity/WatchConnectivity.h>
#interface InterfaceController() <WCSessionDelegate>
{
IBOutlet WKInterfaceDevice *it;
BOOL tday;
IBOutlet WKInterfaceLabel *lblCompany;
}
#end
#implementation InterfaceController
#synthesize myTable = _myTable;
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
// Configure interface objects here.
if([WCSession isSupported]){
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
//[self requestInfoPhone];
[self getToday];
}
- (void)willActivate {
// This method is called when watch view controller is about to be visible to user
[super willActivate];
}
- (void)didDeactivate {
// This method is called when watch view controller is no longer visible
[super didDeactivate];
}
-(void)requestInfoPhone{
NSDictionary *dic = #{#"request":#"ySales"};
[[WCSession defaultSession] sendMessage:dic
replyHandler:^(NSDictionary *replyInfo){
NSLog(#"The Reply: %#", replyInfo);
NSDictionary *location = replyInfo;
NSString *name = location[#"label"];
NSString *totalSales = location[#"totalSales"];
// NSString *test2 = location[#"rowText"];
NSMutableArray *sales = [[NSMutableArray alloc]init];
NSMutableArray *storeNames = [[NSMutableArray alloc]init];
sales = location[#"rowText"];
storeNames = location[#"storeNames"];
[self loadTable:sales names:storeNames company:name];
[_labelName setText:name];
[_labelTotalSales setText:totalSales];
tday = YES;
}
errorHandler:^(NSError *error){
NSLog(#"%#", error);
}
];
}
-(void)loadTable:(NSMutableArray*)tester names:(NSMutableArray*)names company:(NSString *)company{
[_myTable setNumberOfRows:[tester count] withRowType:#"row"];
[_labelName setText:company];
for (int i = 0; i < [tester count]; i++) {
MyRowController *vc = [_myTable rowControllerAtIndex:i];
[vc.testLabel setText:[ftDateParser currencyFormat: tester[i]]];
[vc.nameLabel setText:[ftDateParser parseName:names[i]]];
}
[_myTable scrollToRowAtIndex:(0)];
}
-(IBAction)yesterdaySales:(id)sender{
if (tday) {
[_ydaySales setTitle:#"Today Sales"];
[self requestInfoPhone];
}
else{
[_ydaySales setTitle:#"Yesterday Sales"];
[self getToday];
}
}
-(void)getToday{
NSDictionary *dic = #{#"request":#"todaySales"};
[[WCSession defaultSession] sendMessage:dic
replyHandler:^(NSDictionary *replyInfo){
NSDictionary *location = replyInfo;
NSString *name = location[#"label"];
NSString *totalSales = location[#"totalSales"];
// NSString *test2 = location[#"rowText"];
NSMutableArray *sales = [[NSMutableArray alloc]init];
NSMutableArray *storeNames = [[NSMutableArray alloc]init];
sales = location[#"rowText"];
storeNames = location[#"storeNames"];
[self loadTable:sales names:storeNames company:name];
[_labelName setText:name];
[_labelTotalSales setText:totalSales];
tday = YES;
}
errorHandler:^(NSError *error){
NSLog(#"%#", error);
}
];
}
#end
Parent.m
-(void)setUpAppForWatch{
done = NO;
if([WCSession isSupported]){
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
-(void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message replyHandler:(void (^)(NSDictionary<NSString *,id> * _Nonnull))replyHandler{
/*UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier identifier = UIBackgroundTaskInvalid;
dispatch_block_t endBlock = ^ {
if (identifier != UIBackgroundTaskInvalid) {
[application endBackgroundTask:identifier];
}
identifier = UIBackgroundTaskInvalid;
};
identifier = [application beginBackgroundTaskWithExpirationHandler:endBlock];*/
[self setUpAppForWatch];
[self getTheDate];
startDate = todayDay;
endDate = tomorrow;
//[self getTodaySalesforWatch];
NSString *currency = [ftDateParser currencyFormat:totalSales];
NSDictionary *dic = #{#"label": [NSString stringWithFormat:#"%#", #"Town Crier, Inc."],
#"totalSales": currency,
#"rowText": storeSalesData,//[NSString stringWithFormat:#"%#", currency]
#"storeNames":storeNames
};
NSString *request = [message objectForKey:#"request"];
if ([request isEqualToString:#"todaySales"]) {
[self getTodaySalesforWatch];
}
else if ([request isEqualToString:#"ySales"]){
[self connectToWebService];
}
if (done) {
replyHandler(dic);
}
}
Edit:
Maybe the changes to the parent app were happening before, but I didn't know cause the app was running in the background. Still can't get it to wake the parent app.
You don't link to the source of the quote at the top of your question but it must be referring to the openParentApplication method of WatchKit 1. Devices running WatchOS 2.0 cannot call openParentApplication.
The method you're implementing in the code in your question is for a WCSession, which only works for immediate communication between a WatchKit app extension and an iOS app that are both running at the same time. This method does not cause your iOS app to launch, neither in the background nor in the foreground. Other asynchronous communication methods must be used if both apps are not running at the time.

SplitViewController with a Login View Controller as the root

So I've been researching on how to make a login view controller as the initial view controller instead of the splitview.
Some of the answers I've seen would recommend a modal view to be loaded? I'm not sure how that is set up.
eg.
How to add a login view before a UISplitViewController iPad
and
How to implement SplitViewController on second level.?
So do I add those on the loginviewcontroller class? Or where?
Any advice is welcome.
Thanks!!
I've done this by creating two storyboards: one with the (full-screen) login and one with the split-view.
To switch between them, I've added a custom protocol:
#import <Foundation/Foundation.h>
#protocol RootViewControllerDelegate <NSObject>
-(void)switchToStoryboard: (UIStoryboard *) storyboad animationDirectionOrNil: (NSString *)direction;
#end
The AppDelegate then implements this protocol:
-(void)switchToStoryboard:(id)storyboad animationDirectionOrNil:(NSString *)direction {
UIViewController *newRoot=[storyboad instantiateInitialViewController];
if ([newRoot respondsToSelector:#selector(setRootViewControllerDelegate:)]) {
[newRoot setRootViewControllerDelegate:self];
}
self.window.rootViewController=newRoot;
if(direction){
CATransition* transition=[CATransition animation];
transition.type=kCATransitionPush;
transition.subtype=direction;
[self.window.layer addAnimation:transition forKey:#"push_transition"];
}
}
As you can see, it tries to set itself as the delegate again, so the other view-controller can switch back or to another storyboard. In order for this to work, you would have to subclass UISplitView:
Header
#import <UIKit/UIKit.h>
#import "RootViewControllerDelegate.h"
#interface MySplitViewController : UISplitViewController
#property (nonatomic, weak) id <RootViewControllerDelegate> rootViewControllerDelegate;
#end
iMplementation
#import "MySplitViewController.h"
#implementation MySplitViewController
#synthesize rootViewControllerDelegate;
- (void)viewDidLoad
{
[super viewDidLoad];
for (UIViewController *viewController in self.viewControllers) {
if ([viewController respondsToSelector:#selector(setRootViewControllerDelegate:)]) {
[viewController setRootViewControllerDelegate:self.rootViewControllerDelegate];
}
}
}
#end
This simple implementation looks for child-view-controllers that accept a root-view-controller-delegate and hands it down. So when you want to add a "Show Login"-button to a certain (master- or detail-)view, just create your own UIViewController-subclass, add a #property id<RootViewControllerDelegate> rootViewControllerDelegate and associate an action like this with the button:
- (IBAction)loginButtonClicked:(id)sender {
UIStoryboard *mainSB=[UIStoryboard storyboardWithName:#"LoginStoryboard" bundle:nil];
NSString *animationDirection=kCATransitionFromTop;
UIDeviceOrientation currentOrientation=[[UIDevice currentDevice] orientation];
if (currentOrientation==UIDeviceOrientationLandscapeLeft) {
animationDirection=kCATransitionFromBottom;
}
[self.rootViewControllerDelegate switchToStoryboard:mainSB animationDirectionOrNil:animationDirection];
}
Feel free to adjust everything to your needs.
well here it is my friend. i created a ibaction in in btn and pushed the new view with modal option of the story board. them i plugged in the classes for the login view which also refers to constants that keeps the record strait. then after login is recognized i pushed a new view. bare in mind i was having the users create a password in their device and not importing it from the server. if you want to import it from the server it will be different.
here is the log in .h
#import <UIKit/UIKit.h>
#import "Constants.h"
#interface LogInViewController : UIViewController<UITextFieldDelegate>
#property (nonatomic) BOOL pinValidated;
#end
and here is the code for login .m
#import "LogInViewController.h"
#import "KeychainWrapper.h"
#interface LogInViewController ()
#end
#implementation LogInViewController
#synthesize pinValidated;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)didReceiveMemoryWarning
{
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
// Helper method to congregate the Name and PIN fields for validation.
- (BOOL)credentialsValidated
{
NSString *name = [[NSUserDefaults standardUserDefaults] stringForKey:USERNAME];
BOOL pin = [[NSUserDefaults standardUserDefaults] boolForKey:PIN_SAVED];
if (name && pin) {
return YES;
} else {
return NO;
}
}
- (void)presentAlertViewForPassword
{
// 1
BOOL hasPin = [[NSUserDefaults standardUserDefaults] boolForKey:PIN_SAVED];
// 2
if (hasPin) {
// 3
NSString *user = [[NSUserDefaults standardUserDefaults] stringForKey:USERNAME];
NSString *message = [NSString stringWithFormat:#"What is %#'s password?", user];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Enter Password"
message:message
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Done", nil];
// 4
[alert setAlertViewStyle:UIAlertViewStyleSecureTextInput]; // Gives us the password field
alert.tag = kAlertTypePIN;
// 5
UITextField *pinField = [alert textFieldAtIndex:0];
pinField.delegate = self;
pinField.autocapitalizationType = UITextAutocapitalizationTypeWords;
pinField.tag = kTextFieldPIN;
[alert show];
} else {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Setup Credentials"
message:#"Enter Your information!"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Done", nil];
// 6
[alert setAlertViewStyle:UIAlertViewStyleLoginAndPasswordInput];
alert.tag = kAlertTypeSetup;
UITextField *nameField = [alert textFieldAtIndex:0];
nameField.autocapitalizationType = UITextAutocapitalizationTypeWords;
nameField.placeholder = #"Name"; // Replace the standard placeholder text with something more applicable
nameField.delegate = self;
nameField.tag = kTextFieldName;
UITextField *passwordField = [alert textFieldAtIndex:1]; // Capture the Password text field since there are 2 fields
passwordField.delegate = self;
passwordField.tag = kTextFieldPassword;
[alert show];
}
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (alertView.tag == kAlertTypePIN) {
if (buttonIndex == 1 && self.pinValidated) { // User selected "Done"
[self performSegueWithIdentifier:#"ScoutSegue" sender:self];
self.pinValidated = NO;
} else { // User selected "Cancel"
[self presentAlertViewForPassword];
}
} else if (alertView.tag == kAlertTypeSetup) {
if (buttonIndex == 1 && [self credentialsValidated]) { // User selected "Done"
[self performSegueWithIdentifier:#"ScoutSegue" sender:self];
} else { // User selected "Cancel"
[self presentAlertViewForPassword];
}
}
}
#pragma mark - Text Field + Alert View Methods
- (void)textFieldDidEndEditing:(UITextField *)textField
{
// 1
switch (textField.tag) {
case kTextFieldPIN: // We go here if this is the 2nd+ time used (we've already set a PIN at Setup).
NSLog(#"User entered PIN to validate");
if ([textField.text length] > 0) {
// 2
NSUInteger fieldHash = [textField.text hash]; // Get the hash of the entered PIN, minimize contact with the real password
// 3
if ([KeychainWrapper compareKeychainValueForMatchingPIN:fieldHash]) { // Compare them
NSLog(#"** User Authenticated!!");
self.pinValidated = YES;
} else {
NSLog(#"** Wrong Password :(");
self.pinValidated = NO;
}
}
break;
case kTextFieldName: // 1st part of the Setup flow.
NSLog(#"User entered name");
if ([textField.text length] > 0) {
[[NSUserDefaults standardUserDefaults] setValue:textField.text forKey:USERNAME];
[[NSUserDefaults standardUserDefaults] synchronize];
}
break;
case kTextFieldPassword: // 2nd half of the Setup flow.
NSLog(#"User entered PIN");
if ([textField.text length] > 0) {
NSUInteger fieldHash = [textField.text hash];
// 4
NSString *fieldString = [KeychainWrapper securedSHA256DigestHashForPIN:fieldHash];
NSLog(#"** Password Hash - %#", fieldString);
// Save PIN hash to the keychain (NEVER store the direct PIN)
if ([KeychainWrapper createKeychainValue:fieldString forIdentifier:PIN_SAVED]) {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:PIN_SAVED];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(#"** Key saved successfully to Keychain!!");
}
}
break;
default:
break;
}
}
#pragma mark - View lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
self.pinValidated = NO;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self presentAlertViewForPassword];
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
here is the code for the constant
// Used for saving to NSUserDefaults that a PIN has been set, and is the unique identifier for the Keychain.
#define PIN_SAVED #"hasSavedPIN"
// Used for saving the user's name to NSUserDefaults.
#define USERNAME #"username"
// Used to specify the application used in accessing the Keychain.
#define APP_NAME [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleIdentifier"]
// Used to help secure the PIN.
// Ideally, this is randomly generated, but to avoid the unnecessary complexity and overhead of storing the Salt separately, we will standardize on this key.
// !!KEEP IT A SECRET!!
#define SALT_HASH #"FvTivqTqZXsgLLx1v3P8TGRyVHaSOB1pvfm02wvGadj7RLHV8GrfxaZ84oGA8RsKdNRpxdAojXYg9iAj"
// Typedefs just to make it a little easier to read in code.
typedef enum {
kAlertTypePIN = 0,
kAlertTypeSetup
} AlertTypes;
typedef enum {
kTextFieldPIN = 1,
kTextFieldName,
kTextFieldPassword
} TextFieldTypes;
here is the keychainwrapper
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#import <CommonCrypto/CommonHMAC.h>
#interface KeychainWrapper : NSObject
// Generic exposed method to search the keychain for a given value. Limit one result per search.
+ (NSData *)searchKeychainCopyMatchingIdentifier:(NSString *)identifier;
// Calls searchKeychainCopyMatchingIdentifier: and converts to a string value.
+ (NSString *)keychainStringFromMatchingIdentifier:(NSString *)identifier;
// Simple method to compare a passed in hash value with what is stored in the keychain.
// Optionally, we could adjust this method to take in the keychain key to look up the value.
+ (BOOL)compareKeychainValueForMatchingPIN:(NSUInteger)pinHash;
// Default initializer to store a value in the keychain.
// Associated properties are handled for you - setting Data Protection Access, Company Identifer (to uniquely identify string, etc).
+ (BOOL)createKeychainValue:(NSString *)value forIdentifier:(NSString *)identifier;
// Updates a value in the keychain. If you try to set the value with createKeychainValue: and it already exists,
// this method is called instead to update the value in place.
+ (BOOL)updateKeychainValue:(NSString *)value forIdentifier:(NSString *)identifier;
// Delete a value in the keychain.
+ (void)deleteItemFromKeychainWithIdentifier:(NSString *)identifier;
// Generates an SHA256 (much more secure than MD5) hash.
+ (NSString *)securedSHA256DigestHashForPIN:(NSUInteger)pinHash;
+ (NSString*)computeSHA256DigestForString:(NSString*)input;
#end
and finally here is the code for the keychainwrapper .m
#import "KeychainWrapper.h"
#import "Constants.h"
#implementation KeychainWrapper
// *** NOTE *** This class is ARC compliant - any references to CF classes must be paired with a "__bridge" statement to
// cast between Objective-C and Core Foundation Classes. WWDC 2011 Video "Introduction to Automatic Reference Counting" explains this.
// *** END NOTE ***
+ (NSMutableDictionary *)setupSearchDirectoryForIdentifier:(NSString *)identifier {
// Setup dictionary to access keychain.
NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init];
// Specify we are using a password (rather than a certificate, internet password, etc).
[searchDictionary setObject:( id)kSecClassGenericPassword forKey:( id)kSecClass];
// Uniquely identify this keychain accessor.
[searchDictionary setObject:APP_NAME forKey:( id)kSecAttrService];
// Uniquely identify the account who will be accessing the keychain.
NSData *encodedIdentifier = [identifier dataUsingEncoding:NSUTF8StringEncoding];
[searchDictionary setObject:encodedIdentifier forKey:( id)kSecAttrGeneric];
[searchDictionary setObject:encodedIdentifier forKey:( id)kSecAttrAccount];
return searchDictionary;
}
+ (NSData *)searchKeychainCopyMatchingIdentifier:(NSString *)identifier
{
NSMutableDictionary *searchDictionary = [self setupSearchDirectoryForIdentifier:identifier];
// Limit search results to one.
[searchDictionary setObject:( id)kSecMatchLimitOne forKey:( id)kSecMatchLimit];
// Specify we want NSData/CFData returned.
[searchDictionary setObject:( id)kCFBooleanTrue forKey:( id)kSecReturnData];
// Search.
NSData *result = nil;
CFTypeRef foundDict = NULL;
OSStatus status = SecItemCopyMatching(( CFDictionaryRef)searchDictionary, &foundDict);
if (status == noErr) {
result = ( NSData *)foundDict;
} else {
result = nil;
}
return result;
}
+ (NSString *)keychainStringFromMatchingIdentifier:(NSString *)identifier
{
NSData *valueData = [self searchKeychainCopyMatchingIdentifier:identifier];
if (valueData) {
NSString *value = [[NSString alloc] initWithData:valueData
encoding:NSUTF8StringEncoding];
return value;
} else {
return nil;
}
}
+ (BOOL)createKeychainValue:(NSString *)value forIdentifier:(NSString *)identifier
{
NSMutableDictionary *dictionary = [self setupSearchDirectoryForIdentifier:identifier];
NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:valueData forKey:( id)kSecValueData];
// Protect the keychain entry so it's only valid when the device is unlocked.
[dictionary setObject:( id)kSecAttrAccessibleWhenUnlocked forKey:( id)kSecAttrAccessible];
// Add.
OSStatus status = SecItemAdd(( CFDictionaryRef)dictionary, NULL);
// If the addition was successful, return. Otherwise, attempt to update existing key or quit (return NO).
if (status == errSecSuccess) {
return YES;
} else if (status == errSecDuplicateItem){
return [self updateKeychainValue:value forIdentifier:identifier];
} else {
return NO;
}
}
+ (BOOL)updateKeychainValue:(NSString *)value forIdentifier:(NSString *)identifier
{
NSMutableDictionary *searchDictionary = [self setupSearchDirectoryForIdentifier:identifier];
NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
[updateDictionary setObject:valueData forKey:( id)kSecValueData];
// Update.
OSStatus status = SecItemUpdate(( CFDictionaryRef)searchDictionary,
( CFDictionaryRef)updateDictionary);
if (status == errSecSuccess) {
return YES;
} else {
return NO;
}
}
+ (void)deleteItemFromKeychainWithIdentifier:(NSString *)identifier
{
NSMutableDictionary *searchDictionary = [self setupSearchDirectoryForIdentifier:identifier];
CFDictionaryRef dictionary = ( CFDictionaryRef)searchDictionary;
//Delete.
SecItemDelete(dictionary);
}
+ (BOOL)compareKeychainValueForMatchingPIN:(NSUInteger)pinHash
{
if ([[self keychainStringFromMatchingIdentifier:PIN_SAVED] isEqualToString:[self securedSHA256DigestHashForPIN:pinHash]]) {
return YES;
} else {
return NO;
}
}
// This is where most of the magic happens (the rest of it happens in computeSHA256DigestForString: method below).
// Here we are passing in the hash of the PIN that the user entered so that we can avoid manually handling the PIN itself.
// Then we are extracting the username that the user supplied during setup, so that we can add another unique element to the hash.
// From there, we mash the user name, the passed-in PIN hash, and the secret key (from ChristmasConstants.h) together to create
// one long, unique string.
// Then we send that entire hash mashup into the SHA256 method below to create a "Digital Digest," which is considered
// a one-way encryption algorithm. "One-way" means that it can never be reverse-engineered, only brute-force attacked.
// The algorthim we are using is Hash = SHA256(Name + Salt + (Hash(PIN))). This is called "Digest Authentication."
+ (NSString *)securedSHA256DigestHashForPIN:(NSUInteger)pinHash
{
// 1
NSString *name = [[NSUserDefaults standardUserDefaults] stringForKey:USERNAME];
name = [name stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// 2
NSString *computedHashString = [NSString stringWithFormat:#"%#%i%#", name, pinHash, SALT_HASH];
// 3
NSString *finalHash = [self computeSHA256DigestForString:computedHashString];
NSLog(#"** Computed hash: %# for SHA256 Digest: %#", computedHashString, finalHash);
return finalHash;
}
// This is where the rest of the magic happens.
// Here we are taking in our string hash, placing that inside of a C Char Array, then parsing it through the SHA256 encryption method.
+ (NSString*)computeSHA256DigestForString:(NSString*)input
{
const char *cstr = [input cStringUsingEncoding:NSUTF8StringEncoding];
NSData *data = [NSData dataWithBytes:cstr length:input.length];
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
// This is an iOS5-specific method.
// It takes in the data, how much data, and then output format, which in this case is an int array.
CC_SHA256(data.bytes, data.length, digest);
// Setup our Objective-C output.
NSMutableString* output = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
// Parse through the CC_SHA256 results (stored inside of digest[]).
for(int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[output appendFormat:#"%02x", digest[i]];
}
return output;
}
#end
this is the blueprint for creating a login view before any other view weather it be view controller or any other view such as tab bar or so. take a good look at this set of codes and modify them as you please. hope this helps you my friend. the codes are there all you have to do is study them and modify them to what you want. happy coding.

Facebook Api to check in users in an iOS app

I'm looking to use the Facebook Api to check in users in an iOS app.
I was wondering if I have the permissions set in the AppDelegate but want the user to checkin from a different view controller do I have to declare the Facebook instance in every view controller along with the FBRequestDelegate, FBSessionDelegate, FBDialogDelegate delegate methods? or is it a one time thing in the AppDelegate?
thanks for any help.
I just dealt with the exact same problem. Here is my solution:
I Created a FBRequestWrapper which basically contains the following methods:
#import <Foundation/Foundation.h>
#import "Facebook.h"
#define FB_APP_ID #"xx"
#define FB_APP_SECRET #"xx"
#interface FBRequestWrapper : NSObject <FBRequestDelegate, FBSessionDelegate>
{
Facebook *facebook;
BOOL isLoggedIn;
}
#property (nonatomic, retain) Facebook *facebook;
#property (nonatomic, assign) BOOL isLoggedIn;
+ (id) defaultManager;
- (void) setIsLoggedIn:(BOOL) _loggedIn;
- (void) FBSessionBegin:(id) _delegate;
- (void) FBLogout;
- (void) getFBRequestWithGraphPath:(NSString*) _path andDelegate:(id) _delegate;
- (void) sendFBRequestWithGraphPath:(NSString*) _path params:(NSMutableDictionary*) _params andDelegate:(id) _delegate;
#end
So all the Facebook stuff is managed in this Singleton class.
In my AppDelegate I invoke the authentication. Because in my opinion the authentication has to be done before loading all the controllers.
// FACEBOOK
requestWrapper = [FBRequestWrapper defaultManager];
BOOL loggedIn = [requestWrapper isLoggedIn];
// if the user is not currently logged in begin the session
if (!loggedIn) {
[requestWrapper FBSessionBegin:(id)self];
} else {
NSLog(#"Is already logged in!");
}
// Check if the access token is already there. In that case the user is already authenticated with facebook.
// Directly load the controllers and do not wait till facebook has returned back the access_token
if([[NSUserDefaults standardUserDefaults] objectForKey:#"access_token"] != nil &&
[[NSUserDefaults standardUserDefaults] objectForKey:#"exp_date"] != nil) {
[self loadControllers];
}
You can see that I also store the accessToken in the NSUserDefaults space. Because we are using the accessToken as authentication to our webservice.
My AppDelegate method is delegating the FBSessionDelegate method:
#interface AppDelegate : UIResponder <UIApplicationDelegate, UITabBarControllerDelegate, FBSessionDelegate>
And here is the implementation of the most important method, fbDidLogin:
- (void) fbDidLogin {
NSLog(#"AccessToken: %#", requestWrapper.facebook.accessToken);
[[NSUserDefaults standardUserDefaults] setObject:requestWrapper.facebook.accessToken forKey:#"access_token"];
[[NSUserDefaults standardUserDefaults] setObject:requestWrapper.facebook.expirationDate forKey:#"exp_date"];
[[NSUserDefaults standardUserDefaults] synchronize];
[self loadControllers];
}
Here I store the accesstoken in the NSUserDefaults and load all the Controllers if everything was fine.
Now if you want to access the Facebook Graph Api from any controller outside the AppDelegate you can also use the FBRequestWrapper like that:
- (IBAction)test:(id)sender {
// FBRequestWrapper
NSString *graphPath = #"me/friends";
[[FBRequestWrapper defaultManager] getFBRequestWithGraphPath:graphPath andDelegate:self];
}
Here the code from the FBRequestWrapper.m:
#import "FBRequestWrapper.h"
static FBRequestWrapper *defaultWrapper = nil;
#implementation FBRequestWrapper
#synthesize isLoggedIn, facebook;
+ (id) defaultManager {
if (!defaultWrapper)
defaultWrapper = [[FBRequestWrapper alloc] init];
return defaultWrapper;
}
- (void) setIsLoggedIn:(BOOL) _loggedIn {
isLoggedIn = _loggedIn;
if (isLoggedIn) {
[[NSUserDefaults standardUserDefaults] setObject:facebook.accessToken forKey:#"access_token"];
[[NSUserDefaults standardUserDefaults] setObject:facebook.expirationDate forKey:#"exp_date"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
else {
[[NSUserDefaults standardUserDefaults] setObject:#"" forKey:#"access_token"];
[[NSUserDefaults standardUserDefaults] setObject:#"" forKey:#"exp_date"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (void) FBSessionBegin:(id) _delegate {
if (facebook == nil) {
facebook = [[Facebook alloc] initWithAppId:FB_APP_ID andDelegate:_delegate];
[facebook setSessionDelegate:_delegate];
NSString *token = [[NSUserDefaults standardUserDefaults] objectForKey:#"access_token"];
NSDate *exp = [[NSUserDefaults standardUserDefaults] objectForKey:#"exp_date"];
if (token != nil && exp != nil && [token length] > 2) {
isLoggedIn = YES;
facebook.accessToken = token;
facebook.expirationDate = [NSDate distantFuture];
[self setIsLoggedIn:isLoggedIn];
NSLog(#"Access token: %#", facebook.accessToken);
}
}
if(![facebook isSessionValid]) {
NSArray *permissions = [NSArray arrayWithObjects:#"offline_access", #"read_friendlists", #"user_about_me", nil];
// if no session is available login
[facebook authorize:permissions];
}
}
- (void) FBLogout {
[[NSUserDefaults standardUserDefaults] setObject:#"" forKey:#"access_token"];
[[NSUserDefaults standardUserDefaults] setObject:#"" forKey:#"exp_date"];
[[NSUserDefaults standardUserDefaults] synchronize];
[facebook logout:self];
}
// Make simple requests
- (void) getFBRequestWithGraphPath:(NSString*) _path andDelegate:(id) _delegate {
if (_path != nil) {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
if (_delegate == nil)
_delegate = self;
if(isLoggedIn) {
NSLog(#"is logged in in the get method");
} else {
NSLog(#"Is NOT logged in the get metthod");
}
[facebook requestWithGraphPath:_path andDelegate:_delegate];
}
}
// Used for publishing
- (void) sendFBRequestWithGraphPath:(NSString*) _path params:(NSMutableDictionary*) _params andDelegate:(id) _delegate {
if (_delegate == nil)
_delegate = self;
if (_params != nil && _path != nil) {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
[facebook requestWithGraphPath:_path andParams:_params andHttpMethod:#"POST" andDelegate:_delegate];
}
}
#pragma mark -
#pragma mark FacebookSessionDelegate
- (void)fbDidLogin {
isLoggedIn = YES;
[[NSUserDefaults standardUserDefaults] setObject:facebook.accessToken forKey:#"access_token"];
[[NSUserDefaults standardUserDefaults] setObject:facebook.expirationDate forKey:#"exp_date"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)fbDidNotLogin:(BOOL)cancelled {
isLoggedIn = NO;
}
- (void)fbDidLogout {
[[NSUserDefaults standardUserDefaults] setObject:#"" forKey:#"access_token"];
[[NSUserDefaults standardUserDefaults] setObject:#"" forKey:#"exp_date"];
[[NSUserDefaults standardUserDefaults] synchronize];
isLoggedIn = NO;
}
#pragma mark -
#pragma mark FBRequestDelegate
- (void)request:(FBRequest *)request didFailWithError:(NSError *)error {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
//NSLog(#"ResponseFailed: %#", error);
}
- (void)request:(FBRequest *)request didLoad:(id)result {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
//NSLog(#"Parsed Response: %#", result);
}
/**
* Called after the access token was extended. If your application has any
* references to the previous access token (for example, if your application
* stores the previous access token in persistent storage), your application
* should overwrite the old access token with the new one in this method.
* See extendAccessToken for more details.
*/
- (void)fbDidExtendToken:(NSString*)accessToken expiresAt:(NSDate*)expiresAt {
NSLog(#"Fb did extend token.. NOT IMPLEMENTED YET!");
}
/**
* Called when the current session has expired. This might happen when:
* - the access token expired
* - the app has been disabled
* - the user revoked the app's permissions
* - the user changed his or her password
*/
- (void)fbSessionInvalidated {
NSLog(#"Fb session invalidated.. NOT IMPLEMENTED YET!");
}
#end

Facebook SDK Singleton in xcode auto login before post?

I have had the Facebook SDK working in the APPDelegate as instructed by the Facebook tutorial, however I am trying to put in into a singleton method. Every tutorial i've found seems to be for an older version of the SDK, but I have managed success with this one http://indiedevstories.com/2011/08/30/a-facebook-reusable-class/
I'm having 2 problems, the first is posted here this is the second:
I want a button to post to Facebook, but if the user isn't logged in then they need to login first then post (without having to press a separate login button first).
I can log in fine, and I can post fine, however I can't do both together.
If not logged in, the post code shows the login screen, but doesn't goto the post screen after logging in. you have to press post again.
If you try to login but are already logged in, then nothing happens.
So on the button I use code to login then post, as the login is just skipped if already logged in.
The problem i'm having, is the post code is being run instantly after the login code, so before the user has had a chance to login. This results in 2 popups being opened (1 login and 1 post which displays login as not logged in yet).
How can I get my code to wait for the user to login before moving onto the next line in the code to post?
FacebookHelper.h
#interface FacebookHelper : NSObject <FBSessionDelegate, FBRequestDelegate, FBDialogDelegate, FBLoginDialogDelegate> {
Facebook *_facebook;
NSArray *_permissions;
}
#property (nonatomic,strong) Facebook *facebook;
+(FacebookHelper *) sharedInstance;
+(void)fbDidLogin;
#pragma mark - Public Methods
-(BOOL) isFBSessionValid;
-(void) login;
-(void) logout;
-(void) postToWallWithDialogNewHighscore:(int)highscore;
#end
FacebookHelper.m
#implementation FacebookHelper
#synthesize facebook;
#pragma mark -
#pragma mark Singleton Variables
static FacebookHelper *singletonDelegate = nil;
#pragma mark -
#pragma mark Singleton Methods
+(FacebookHelper *)sharedInstance
{
#synchronized(self) {
if (singletonDelegate == nil) {
singletonDelegate = [[self alloc] init]; // Assignment not done here
}
}
return singletonDelegate;
}
-(id)init
{
self = [super init];
if (self) {
_facebook = [[Facebook alloc] initWithAppId:kAppId andDelegate:self];
// Restore previous session
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:#"FBAccessTokenKey"] && [defaults objectForKey:#"FBExpirationDateKey"]) {
_facebook.accessToken = [defaults objectForKey:#"FBAccessTokenKey"];
_facebook.expirationDate = [defaults objectForKey:#"FBExpirationDateKey"];
}
//
}
return self;
}
+(id)allocWithZone:(NSZone *)zone
{
#synchronized(self) {
if (singletonDelegate == nil) {
singletonDelegate = [super allocWithZone:zone];
// assignment and return on first allocation
return singletonDelegate;
}
}
//on subsequent allocation attemps, return nil
return nil;
}
-(id)copyWithZone:(NSZone *)zone
{
return self;
}
#pragma mark - Facebook Delegate Methods
-(void)fbDidLogin
{
NSLog(#"fbDidLogin");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[_facebook accessToken] forKey:#"FBAccessTokenKey"];
[defaults setObject:[_facebook expirationDate] forKey:#"FBExpirationDateKey"];
[defaults synchronize];
}
-(void)fbDidLogout
{
NSLog(#"fbDidLogout");
// Remove saved authorisation information if it exists
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults objectForKey:#"FBAccessTokenKey"]) {
[defaults removeObjectForKey:#"FBAccessTokenKey"];
[defaults removeObjectForKey:#"FBExpirationDateKey"];
[defaults synchronize];
}
}
#pragma mark - Public Methods
-(NSMutableDictionary *) buildPostParamsWithHighScore:(int)highscore
{
NSString *customMessage = [NSString stringWithFormat:kCustomMessage, highscore, kAppName];
NSString *postName = kAppName;
NSString *serverLink = [NSString stringWithFormat:kServerLink];
NSString *imageSrc = kImageScr;
//Final params build
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
//#"message", #"message",
imageSrc, #"picture",
serverLink, #"link",
postName, #"name",
#" ", #"caption",
customMessage, #"description",
nil];
return params;
}
-(BOOL) isFBSessionValid
{
// Check if there is a valid session
//_facebook = [[Facebook alloc] initWithAppId:kAppId andDelegate:self];
_facebook.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:#"FBAccessTokenKey"];
_facebook.expirationDate = [[NSUserDefaults standardUserDefaults] objectForKey:#"FBExpirationDateKey"];
NSLog(#"accessToken=%# expirationDaate=%#",_facebook.accessToken,_facebook.expirationDate);
if (![_facebook isSessionValid]) {
NSLog(#"FacebookHelper isFBSessionValid = NO");
return NO;
} else {
NSLog(#"FacebookHelper isFBSessionValid = YES");
return YES;
}
return NO;
}
-(void) login
{
NSLog(#"FacebookHelper login");
_permissions = [NSArray arrayWithObjects:#"publish_stream", nil]; //#"read_stream", #"offline_access"
[_facebook authorize:_permissions];
}
-(void) logout
{
[_facebook logout];
}
-(void) postToWallWithDialogNewHighscore:(int)highscore
{
NSMutableDictionary *params = [self buildPostParamsWithHighScore:highscore];
NSLog(#"Post Feed");
[_facebook dialog:#"feed" andParams:params andDelegate:self];
}
#end
Button Action:
- (IBAction)facebookTest:(id)sender {
[[FacebookHelper sharedInstance] login];
[[FacebookHelper sharedInstance] postToWallWithDialogNewHighscore:123];
}
I have updated the singleton class to post scores to Facebook Wall. Check out the new version: http://indiedevstories.com/2012/04/11/facebookscorer-post-highscores-to-users-facebook-wall/
This new version handles correctly the internal state when authorization and login is required.
HTH
What you need to do is have your postToWallWithDialogNewHighscore call the login, if necessary, and then wait to hear back from the FBSession delegate. Then you need to setup your facebook fbDidLogin delegate so that when it is done it sends back a message. There are a number of ways to do this (a post-login selector, NSNotificationCenter, etc). Something like this should work (note I am assuming an ivar of NSMutableDictionary *postParams for convenience):
-(void)doPost {
NSLog(#"Post Feed");
[_facebook dialog:#"feed" andParams:postParams andDelegate:self];
}
-(void)postToWallWithDialogNewHighscore:(int)highscore
{
postParams = [self buildPostParamsWithHighScore:highscore];
if (_facebook.isSessionValid) {
[self doPost];
} else {
//register an observer for FB login messages
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(postToWallObserver:) name:#"FBLoginComplete" object:nil];
[self login];
}
}
-(void)fbDidLogin {
NSLog(#"token granted until %#", [_facebook expirationDate]);
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[_facebook accessToken] forKey:#"FBAccessTokenKey"];
[defaults setObject:[_facebook expirationDate] forKey:#"FBExpirationDateKey"];
[defaults synchronize];
[[NSNotificationCenter defaultCenter] postNotificationName:#"FBLoginComplete" object:nil];
}
-(void)fbDidNotLogin:(BOOL)cancelled {
//do nothing as they didn't login
}
-(void) postToWallObserver:(NSNotification *) notification {
if ([[notification name] isEqualToString:#"FBLoginComplete"]) {
if ([_facebook isSessionValid]) {
[self doPost];
}
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Note: I have not compiled this code so there may be typos, etc.