How to calculate ‘Tomorrow at 4 pm’ in iOS

NSCalendar *calendar = [NSCalendar gregorianCalendar];
NSDateComponents *oneDayComponents = [[NSDateComponents alloc] init];
oneDayComponents.day = 1;
NSDate *tomorrow = [calendar dateByAddingComponents:oneDayComponents toDate:[NSDate date] options:nil];
NSUInteger unitFlags = NSEraCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
NSDateComponents *tomorrowAt4PM = [calendar components:unitFlags fromDate:tomorrow];
tomorrowAt4PM.hour = 16;
tomorrowAt4PM.minute = 0;
tomorrowAt4PM.second = 0;
NSDate *alarmDate = [calendar dateFromComponents:tomorrowAt4PM];

http://adcdownload.apple.com//wwdc_2012/wwdc_2012_session_pdfs/session_304__events_and_reminders_in_event_kit.pdf

Advertisements

Facebook sharing feature in iOS app

Classic/Obsolete(?) way

Install SDK

Drag framework folder into XCode project.

Drag bundle into XCode project.

Drag deprecated header folder into XCode project. check copy in needed.

When picking dependency libraries, set them to optional, to allow app running on iOS5.

Set other link flag to -lsqlite3.0

Those steps are fully documented on facebook developer site: https://developers.facebook.com/docs/getting-started/facebook-sdk-for-ios/3.1/

Note: we are using old fashion Facebook sharing feature in app.  (FBDelegate vs. FBRequest) Because my client doesn’t like posting story on their wall without a preview. This way app will popup a separated window to display link to share, image, title, description, etc. User needs to type something in the textbox with placeholder ‘say something about this…’

Note: again, preset message feature has been disabled by Facebook as of 2012.

In app, we are adding #include “Facebook.h” to implement this.

Note: you can use sharekit library for this, but I want to figure out what’s happening behind the scene.

In the place you want to popup Facebook share options to user, do this to open a Facebook session.

// openSessionWithAllowLoginUI will be called by the actual sharing screen where users can click button to post message on their wall.
- (BOOL)openSessionWithAllowLoginUI:(BOOL)allowLoginUI
{
    NSArray *permission = [[NSArray alloc] initWithObjects:@"user_likes",@"publish_actions", nil];

    return [FBSession openActiveSessionWithPublishPermissions:permission
                                              defaultAudience:FBSessionDefaultAudienceFriends
 allowLoginUI:allowLoginUI
                                            completionHandler:^(FBSession *session, FBSessionState status, NSError *error) {
                                                [self sessionStateChanged:session state:status error:error];
                                            }];}

NSString *const FBSessionStateChangedNotification = @"com.bitrixsoft.Login:FBSessionStateChangedNotifiction";

- (void)sessionStateChanged:(FBSession *)session
                    state:(FBSessionState) state
                    error:(NSError *)error
{
    switch (state) {
        case FBSessionStateOpen:
            if (!error) {
                NSLog(@"User session found");
            }
            break;
        case FBSessionStateClosed:
        case FBSessionStateClosedLoginFailed:
            [FBSession.activeSession closeAndClearTokenInformation];
        default:
            break;
    }

    [[NSNotificationCenter defaultCenter]
     postNotificationName:FBSessionStateChangedNotification
     object:session];

    if (error) {
        [[[UIAlertView alloc]
         initWithTitle:@"Error"
         message:error.localizedDescription
         delegate:nil
         cancelButtonTitle:@"OK"
          otherButtonTitles:nil] show];
    }
}

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    return [FBSession.activeSession handleOpenURL:url];
}

I put this in appDelegate, so it can be shared. I don’t want to call this in appDelegate, otherwise the redirect to facebook to ask permission/login will annoy users.

Add those in appDelegate.h file

extern NSString *const FBSessionStateChangedNotification;

- (BOOL)openSessionWithAllowLoginUI:(BOOL)allowLoginUI;

Before calling opensession, sharing window should subscribe statechange notification.

[[NSNotificationCenter defaultCenter]
     addObserver:self
     selector:@selector(sessionStateChanged:)
     name:FBSessionStateChangedNotification
     object:nil];

- (void)sessionStateChanged:(NSNotification *)notification
{
    if ((FBSession.activeSession.isOpen)) {

        if (nil == self.facebook) {
            self.facebook = [[Facebook alloc]
                             initWithAppId:FBSession.activeSession.appID
                             andDelegate:nil];

            self.facebook.accessToken = FBSession.activeSession.accessToken;
            self.facebook.expirationDate = FBSession.activeSession.expirationDate;

            // resume sharing routine here, because first time attempt without login will stop after app back from background, 
            // but this method will get statechange notifcation, so perfect to resume/restart here.
            [self shareViaFacebook];
        }
    }else{

        self.facebook = nil;
    }

Finally, the actual post action:

- (IBAction)shareOptions:(id)sender
{

    BNAppDelegate * appDelegate = (BNAppDelegate*)[[UIApplication sharedApplication] delegate];
    [appDelegate openSessionWithAllowLoginUI:YES];

    UIActionSheet * options = [[UIActionSheet alloc]
                               initWithTitle:@"Share"
                               delegate:self
                               cancelButtonTitle:@"Cancel"
                               destructiveButtonTitle:nil
                               otherButtonTitles:@"Twitter", @"Facebook", @"email", nil];

    [options showInView:self.view];
}
- (void)shareViaFacebook
{
    NSString * message = @"test msg from my test ios app";

    // no nil allow in any parameter
    NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                   @"Bitrix News iPhone app", @"name",
                                   _post.title, @"caption",
                                   _post.exerpt, @"description",
                                   @"http://www.frankmao.com", @"link",
                                   @"http://ww1.prweb.com/prfiles/2011/02/01/8151699/gI_60506_bxlogo.jpg", @"picture",
                                   message, @"message", //already obsolete, don't bother, facebook will ignore it anyway
                                   nil];

    [self.facebook dialog:@"feed" andParams:params andDelegate:self];

}

Someother gotcha, app identifier must match between app and setting in developers.facebook.com/app, otherwise facebook will complain app is misconfigured when starting the session.

REF: https://developers.facebook.com/docs/reference/dialogs/feed/

https://developers.facebook.com/docs/howtos/feed-dialog-using-ios-sdk/

Push notification coding memo

Push messages are all from apple push server, one for each environment: sandbox and production.

You need to build a push client to send message to Apple push server in PHP or ruby. Push client needs ssl certificate from apple. To get it, enable push feature in your AppId, import cert, then create new provision file.

Considering Xtify or UrbanAirship if you or your clients don’t have your/their own VPS. Both Xtify and UrbanAirship offer free tier membership, 1 million push messages per month.

Documentation from Xtify and UrbanAirship are a little bit different, Xtify needs you export private key under APN certificate item as push certificate, while UrbanAirship document stated that you need to export the APN certificate itself (not the private key under), otherwise when exporting certificate, it will report error saying you are importing a development certificate.

(Update at 2012-07-21: I couldn’t get message sent from Xtify server for my new app, re-export the dev certificate from root instead from child, upload this certificate, problem solve. This is consistent with UrbanAireship then, while the Xtify doc did state this very clear, Right click and export this key as .p12 format. should be right click on ‘Apple Development iOS push service’ to export, not on your name.)

Xtify doesn’t need credit card to register, UrbanAirship won’t allow you create App for production until you give away your credit card number.

Xtify has a magic schedule settings, forgetting set this will cause not sending message, ensure to activate it and check measurement report before you revoke your certificate and start over.

Support response from UrbanAirship is mush faster, my first question was answered in 3 hours, amazing.

Call Apple feedback service frequently to eliminate inactive device from your send out list.

Invalid Product Id?

I’ve re-visited this same blog post again and again at least 10 times since I started iOS developing, every time with different answer, to save me search time in the future, I quoted the list here and added my 2 cents for how to solve invalid-product-id problem.

List from Troy:

  • Have you enabled In-App Purchases for your App ID?
  • Have you checked Cleared for Sale for your product?
  • Have you submitted (and optionally rejected) your application binary? (I don’t think this is necessary, in fact, after you submit, you can add new product into the app anymore)
  • Does your project’s .plist Bundle ID match your App ID?  (This is what I got today, App ID and bundle ID were created by another guy)
  • Have you generated and installed a new provisioning profile for the new App ID? (got me 2 times)
  • Have you configured your project to code sign using this new provisioning profile? (got me 1 time)
  • Are you building for iPhone OS 3.0 or above?
  • Are you using the full product ID when when making an SKProductRequest? (got me 1 time)
  • Have you waited several hours since adding your product to iTunes Connect? (got me 1 time)
  • Are your bank details active on iTunes Connect? (via Mark)
  • Have you tried deleting the app from your device and reinstalling? (via Hector, S3B, Alex O, Joe, and Alberto) (This solved my problem 3 times)
  • Is your device jailbroken? If so, you need to revert the jailbreak for IAP to work. (via oh my god, Roman, and xfze)

 

My suggestions:

  1. Test listing product from simulator first, then move on to device. Ensure product id is OK.
  2. Uninstall can fix the problem happened on device most of time.
  3. Delay on new created product happens, at least to me 2 times, but not always, check your local settings carefully following the list above.
  4. Once old product id being removed, the new product id still can not conflict with the old id, it’s a soft delete in Apple’s system.
  5. Don’t bother auto-renewable subscription product, unless your are building a newsstand/magazine app.
  6. Don’t be too greedy on consumable in-app purchase product, start small, you can add big one in later version. Apple is very strict on those coin-like products.

Build an iOS app access only Drupal CMS site

Drupal is based on mysql, in the new version 7 it gives option to choose sqlite. Either way, connecting from iOS app directly to Drupal db is a problem.

Somebody posted mysql library for mac OS, even for iSO, but it’s not public API. Apple won’t like it.

Solution: RESTful service on Drupal

Installation process is painful, because the missing sypc.php can’t be done through Update management web interface module.

Turn on RESTful service, getting node/1 is not a problem, but I couldn’t make user/1 to work, later I realized it’s permission problem, had to grant ‘pull user profile’ right to anonymous user.

The official doc stated that RESTful comes with basic authentication sub module, but I couldn’t find it, the only one available in RESTful config panel is Session Authentication, which means admin must log in to be able to call user/1

Tried http://username:passwrod@mysite/test/user/1, and using code to pass credential, like this code in ruby,  no good.


uri = URI('http://test.site.com/drupal/rest/user/1.json')

req = Net::HTTP::Get.new(uri.request_uri)

req.basic_auth 'user', 'psw'

res = Net::HTTP.start(uri.host, uri.port) {|http|
http.request(req)
}
p res
puts res.body

It seems Drupal RESTful service is having problem in basic authentication. Or I just too dumb to figure it out.

My project doesn’t have a requirement to protect web content, which means those nodes should be only available to iOS client.

Work Around: Secure site module

One problem to fix is to apply patch for non-Apache-module mode php site, mine is CGI, adding

RewriteRule .* – [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]

to .htaccess, tah-dah, it works!

While the database setup in secure site module is still too messy to me, I just end up with setup a highly secured anonymous user for now. Anyway, content has been protected.

About testing Drual REST servcie

GET is fine, but http://test.site.com/drupal/rest/taxonomy_term/selectNodes is a POST only URI.

Test method 1: download Poster Firfox add-on, set Content Type to application/x-www-form-urlencoded, parameter to tid=1

To get json result, set headers to application/json.

Test method 2:

curl http://test.site.com/drupal/rest/taxonomy_term/selectNodes –form ‘tid=1’ -u user:pwd –header ‘Accept:application/xml’

Watch out, by default form-urlencoded is not checked after installation.

Passing json as request data is much easier.

curl http://test.site.com/drupal/rest/taxonomy_term/selectNodes –d {‘tid’:1} -u user:pwd –header ‘Accept:application/json’

doesn’t work for me, don’t know what missing.

Back to Session Authentication

According to this comment, to properly setup a session in RESTfule service is this:

  1. Get session id through an empty body POST to rest/system/connect, parse response to get session id. (Must set req[‘Accept’] = ‘application/json’ or xml if using ruby)
  2. POST to rest/user/login with parms like sessid=2c038ba6326f12f132502e98434857cc&username=<username>&password=<password>
  3. Parse body.

This works, but how to keeping session in the following rest request is not clear, more specific, how to pass sess id in request?

Here is another way based on Michael Cole’s comment, calling login directly, his example is saving session name/id in a cookie file, then passing it as session cookie in the following request. If actually can be passed as Cookie attribute in header.

Here is my explanation in ruby code,


  def login(username, psw)
    uri = URI("http://test.mysite.com/drupal/rest/u/login.json")

    req = Net::HTTP::Post.new(uri.request_uri)

    req.content_type = 'application/x-www-form-urlencoded'
    req.set_form_data("username" => username, 'password' => psw)
    #req['Accept'] = 'application/json'  #another style if url not end with .json

    res = Net::HTTP.start(uri.host, uri.port) {|http|
      http.request(req)
    }

    if Net::HTTPOK
       json = JSON(res.body)

      @session_id = json["sessid"]
      @session_name = json["session_name"]
    end
  end

  def get_post(id)
    uri = URI("http://test.mysite.com/drupal/rest/node/#{id}.json")

    req = Net::HTTP::Get.new(uri.request_uri)
    req["Cookie"] =  "#{@session_name}=#{@session_id}"
    res = Net::HTTP.start(uri.host, uri.port) {|http|

      http.request(req)
    }

    if Net::HTTPOK

       json = JSON(res.body)

      return json["body"]["und"][0]["safe_value"]
    end

  end

Best doc I found on drupal REST service.

Sqlite and CoreData in iOS, repository pattern

I haven’t see too many articles talking about iOS Core-Data development yet, so I documented something important here for future reference.

  1.  No need to create ID property as identity column for your model, in fact, id is a reserved keyword for cocoa, IDE will looks weird for id property in model. core-data will generate a few extra columns in your model table, including ZID, Z…, all  have a Z prefix.
  2. All number like fields will be mapped to NSNumber.
  3. Create app with core-data support, in XCode4, single view app template doesn’t have this core-data support option, try create another project in different project template, then copy the generated core-data code from appDelegate into your project.
  4. If you have your own existing database which will be installed with app bundle, you need to manually copy it from bundle to document folder to make it writable.
  5. New object needs to save into database later should created by insertNewObjectForEntityForName.
  6. Hold the managedObjectContext till the app terminate, that’s why core-data code were in appDeledate, if move into you own class, create retain property.

Personnly, I like to implement repository pattern for all coredata operations, including initialize. My sample repository looks like this:

@interface iCWOccurrenceRepository()
- (NSMutableArray *)retrieveAllOccurrencesForCity:(NSString*)city sinceDate:(NSDate*)startDate;
@end

@implementation iCWOccurrenceRepository

@synthesize managedObjectContext = __managedObjectContext;
@synthesize managedObjectModel = __managedObjectModel;
@synthesize persistentStoreCoordinator = __persistentStoreCoordinator;

NSString * DBNAME = @"mydb.sqlite";

- (void)saveANew:(NSArray*) line{
    NSManagedObjectContext * context = self.managedObjectContext;
    NSManagedObject *newOccurrence = [NSEntityDescription
                                      insertNewObjectForEntityForName:@"Occurrence"
                                      inManagedObjectContext:context];

    Occurrence* oToSave = ((Occurrence*)newOccurrence);
    oToSave.id = [NSNumber numberWithInt:[[line objectAtIndex:0] intValue]];
    oToSave.city = [line objectAtIndex:1];
//...
    [self saveContext];

}
- (void)saveContext
{
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = __managedObjectContext;
    if (managedObjectContext != nil)
    {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
        {
            /*
             Replace this implementation with code to handle the error appropriately.

             abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
             */
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

#pragma mark - Core Data stack

/**
 Returns the managed object context for the application.
 If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
 */
- (NSManagedObjectContext *)managedObjectContext
{
    if (__managedObjectContext != nil)
    {
        return __managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    {
        __managedObjectContext = [[NSManagedObjectContext alloc] init];
        [__managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return __managedObjectContext;
}

/**
 Returns the managed object model for the application.
 If the model doesn't already exist, it is created from the application's model.
 */
- (NSManagedObjectModel *)managedObjectModel
{
    if (__managedObjectModel != nil)
    {
        return __managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"iCW" withExtension:@"momd"];
    __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return __managedObjectModel;
}

/**
 Returns the persistent store coordinator for the application.
 If the coordinator doesn't already exist, it is created and the application's store added to it.
 */
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:DBNAME];

    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
    {
         [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return __persistentStoreCoordinator;
}

#pragma mark - Application's Documents directory

/**
 Returns the URL to the application's Documents directory.
 */
- (NSURL *)applicationDocumentsDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

-(void)createEditableCopyOfDatabaseIfNeeded {
	//
	NSFileManager *fileManager = [NSFileManager defaultManager];
	NSURL *documentDirectory = [self applicationDocumentsDirectory];
	NSString *writeableDBPath = [[documentDirectory
								  URLByAppendingPathComponent:DBNAME]
								 path];
	NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath]
							   stringByAppendingPathComponent:DBNAME];

	BOOL defaultDbExists = [fileManager fileExistsAtPath:defaultDBPath];
	BOOL dbExistsInDocFolder = [fileManager fileExistsAtPath:writeableDBPath];

	if (!dbExistsInDocFolder && defaultDbExists) {

		NSError *error;
		BOOL success = [fileManager copyItemAtPath:defaultDBPath toPath:writeableDBPath error:&error];

		if (!success) {
            NSAssert1(0, @"Failed to create writable database file with message '%@'.",
                      [error localizedDescription]);
		}
	}

}

- (NSMutableArray *)retrieveAllOccurrencesForCity:(NSString*)city sinceDate:(NSDate*)startDate{

	NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
	NSFetchRequest *request = [[NSFetchRequest alloc] init];
	NSEntityDescription *entity = [NSEntityDescription entityForName:@"Occurrence"
											  inManagedObjectContext:managedObjectContext];
	[request setEntity:entity];

	NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
										initWithKey:@"id" ascending:NO];

	NSArray *sortDescriptors = [[NSArray alloc]
								initWithObjects:sortDescriptor, nil];

	[request setSortDescriptors:sortDescriptors];

	NSPredicate *bPredicate =
    [NSPredicate predicateWithFormat:@"city == %@ and date > %@", city, startDate];

    [request setPredicate:bPredicate];

	NSError *error;
	NSMutableArray *fetchResults = [[managedObjectContext executeFetchRequest:request
                                                                        error:&error] mutableCopy] ;

	if (fetchResults == nil) {
		// handle the error
	}
	return fetchResults;

}
@end

Again, this repository class needs to be a strong retain property in caller.

MPMoviePlayerController

I need implement the following two features in most of my iOS apps, e.g., iClip:

  • auto-play-next-episode if available
  • auto resume from the stop point last time stopped/pasued

By looking into the developers doc, the first one is easy to do, just observe the MPMoviePlayerPlaybackDidFinishNotification:


-(void)playMovieAtURL:(NSURL*)theURL
{

	MPMoviePlayerViewController* mpvc =[[MPMoviePlayerViewController alloc] initWithContentURL:theURL];
	[self presentModalViewController:mpvc animated:NO];

	double stoppedPoint = [[NSUserDefaults standardUserDefaults] doubleForKey:[mpvc.moviePlayer.contentURL absoluteString]];
	if (stoppedPoint > 0.0) {
		mpvc.moviePlayer.initialPlaybackTime = stoppedPoint;
	}

	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(myMovieFinishedCallback:)
												 name:MPMoviePlayerPlaybackDidFinishNotification
											   object:mpvc.moviePlayer];

	if (![self isOS5])
	{
		mpvc.moviePlayer.useApplicationAudioSession = NO;
	}

	self.mpvc = mpvc; //retain it
	[mpvc release];
	return;

}

// When the movie is done,release the controller.
-(void)myMovieFinishedCallback:(NSNotification*)aNotification
{

    if ([[aNotification object] isKindOfClass:[MPMoviePlayerController class]])
    {
        MPMoviePlayerController* theMovie=[aNotification object];
        [[NSNotificationCenter defaultCenter] removeObserver:self
                                                        name:MPMoviePlayerPlaybackDidFinishNotification
                                                      object:theMovie];

        [self  dismissModalViewControllerAnimated:NO];

        theMovie = nil;
        [theMovie release];
    }

    NSDictionary *userInfo = [aNotification userInfo];

    int reason = [[userInfo objectForKey:@"MPMoviePlayerPlaybackDidFinishReasonUserInfoKey"] intValue];
    NSLog(@"clip finished because %d. play next one if available.", reason);
    // ios 4 always return MPMovieFinishReasonPlaybackEnded even it's user exit it, a bug. don't provide play next feature for ios4 users then.
    if ([self isOS5] && reason == MPMovieFinishReasonPlaybackEnded)
    {
        [self playNextEpisodeIfAvailable];
    }
}

Notice the finished reason in iOS 4 (bug) is always MPMovieFinishReasonPlaybackEnded, I had to implement the same feature in MPMoviePlayerPlaybackStateDidChangeNotification for iOS 4 users:


-(void)myMovieStateChangedCallback:(NSNotification*)aNotification
{
    if ([[aNotification object] isKindOfClass:[MPMoviePlayerController class]])
    {
        MPMoviePlayerController* theMovie=[aNotification object];
        if (theMovie.playbackState == MPMoviePlaybackStateStopped && theMovie.currentPlaybackTime == theMovie.playableDuration) {
            NSLog(@"clip finished!");

            if ([self isOS5]) {

            }else{
                [self playNextEpisodeIfAvailable];
            }
        }

For second feature, saving stop point when movie exit, I also use the MPMoviePlayerPlaybackStateDidChangeNotification to catch the currentPlaybackTime, I’ve tried catch it in MPMoviePlayerPlaybackDidFinishNotification, the value is zero, too late.