SlideShare a Scribd company logo
1 of 95
Google Calendar Integration with iOS App
www.letsnurture.com
www.letsnurture.com
Project Overview
The app we'll build in this tutorial is going to let users
get connected to their Google account, download their
calendars, and create a new event with a description
and a date/time. The new event will be posted to a
calendar that the user selects.
Regarding our app structure, the basic view is going to
be a table view that will contain three sections for
setting the following data:
An event description
An event date/time
A target calendar
www.letsnurture.com
As far as the event description is concerned, a textfield will
appear on the cell when the user edits the description and it will
go away when he finishes doing so. For ease of use, an Input
Accessory View will appear above the keyboard every time that
the textfield is displayed.
For setting or changing the event date and time, another view
(not a view controller) is going to be used. This view will contain
a date picker view, from which the user will be able to pick a
date.
An event can be an all-day event, so no specific time needs to
be set. For this case, a button will be used to set the date picker
contents. For events occurring at a specific time, the date picker
will display both date and time. For all-day events, only the date
will be displayed. The view that contains the date picker (and a
toolbar with the necessary bar button items as well) will be
added as a subview to the view of our view controller when the
user taps on the row of the second section of the table view.
www.letsnurture.com
Finally, the calendar section is going to be multi-functional.
When the user is not yet signed into their Google account,
only one row is going to exist in the section with a message
that prompts the user to download their calendars. When the
user selects this, the authorization process will be put in
action.
Once the access token has been obtained, an API call will be
made to get the calendar list from Google. After the calendar
list has been taken, the first calendar on the list is going to
replace the prompting message on the row and become the
selected calendar by default.
In addition to all of the above, a toolbar will exist under the
table view and it will contain two bar button items. One button
will be for posting the event on the selected calendar and a
second button will be used to log out.
www.letsnurture.com
1. Create a New Project
Step 1
Launch Xcode and create a new project. Select the Single View Application option
and click Next:
www.letsnurture.com
In the Product Name field, add the GoogleCalendarPostDemo value. Of
course, you may choose another name if you'd like.
Also, make sure to check the Use Automatic Reference Counting option and
uncheck everything else. After that, keep on going.
www.letsnurture.com
Finally, select a directory to store the project and click Create.
www.letsnurture.com
Step 2
Now you need to add the class files that were implemented in the
previous tutorial. This class is going to be the mechanism that will do all
the work behind the scenes. So, if you have the previous tutorial's project
files, get these two:
GoogleOAuth.h
GoogleOAuth.m
Add them into your project. If you don't have the previous project, then
you can download and get them from this post's download.
www.letsnurture.com
2. Building the Interface
We will use Interface Builder to setup the interface. As you will soon find
out, several subviews are going to be added because we want to make
the demo app as functional as it can be. Here is a series of steps that
describe every subview you should add, along with the properties for
each one:
Open the ViewController.xib file to configure the interface and to add all
the necessary subviews. First of all, set the view's Size to None in the
Utilities Pane > Attributes Inspector > Simulated Metrics, in order to let
the project work properly on iPhones prior to the 5.
www.letsnurture.com
Set the view's Background Color to White.
Drag-and-drop a UIToolbar subview into the view. Place it at the bottom
of the screen.
Add the following items on the toolbar (ordered left-to-right):
Bar Button Item with Title: Sign out
Flexible Space Bar Button Item
Bar Button Item with Title: Post
Add a UITableView subview on the view and let it occupy all the
available space left on the view.
Set the next two properties of the table view:
Style: Grouped
Background:: Clear Color
www.letsnurture.com
www.letsnurture.com
Next, we need to have another view that will contain the date picker
view. Here are the steps:
Add a new view outside of the default view and set its Size to None
(like you did before). Also, set its Height to 460.
Set the view's Background Color to Scroll View Textured Background
Color.
Add a UIDatePicker subview in the view and center it in accordance to
the view's center.
Add a UIToolBar subview at the bottom of the view.
Add the following bar button items to the toolbar:
Bar Button Item with Title: Cancel
Flexible Space Bar Button Item
Bar Button Item with Title: All-day event
Flexible Space Bar Button Item
Bar Button Item with Title: Okay
www.letsnurture.com
www.letsnurture.com
Finally, add the following subviews outside the default view
controller's view and in the second view we just created:
A UIToolBar subview. This is going to be the Input Accessory
View for the textfield that will be used to edit the event
description. Add the next bar button items to it:
Bar Button Item with Title: Cancel
Flexible Space Bar Button Item
Bar Button Item with Title: Okay
A UIActivityIndicatorView with the following properties:
Style: Large White
Background: Black Color
www.letsnurture.com
3. Setup IBOutlet Properties & IBAction Methods
Step 1
We are going to need a few IBOutlet properties connected to our subviews, so
we can modify them in code.
To connect an IBOutlet property to a subview (as well as to create an IBAction
method), you need to have the ViewController.h file shown in the Assistant
Editor.
So, click on the middle button of the Xcode Editor toolbar to let it show up.
Make sure that the contents of the ViewController.h file are displayed there.
www.letsnurture.com
I will show you how to create an IBOutlet property for the table view
only. Use the same way to create the properties for the subviews I'll
tell you about next.
Either on the Document Outline pane or directly on the view, do the
following:
1.Right-Click or Control-Click on the table view
2.On the black popup menu, click on the circle next to the New
Referencing Outlet option and keep the mouse button pressed.
3.Drag-and-drop (and at the same time a blue line will follow your
mouse) into the Assistant Editor window.
www.letsnurture.com
On the new window that appears, add the tblPostData as the Name of the
property and don't touch any other options.
Click on the Connect button or hit the Return button on your keyboard.
Here is a list with all the subviews that we need IBOutlet connections created,
along with their names. Make sure to follow the same way as before and you'll
be fine.
•Post bar button item: barItemPost
•Sign out bar button item: barItemRevokeAccess
•Input Accessory View Toolbar (the alone toolbar): toolbarInputAccessoryView
•View container of the date picker: viewDatePicker
•Date Picker: dpDatePicker
•All-day bar button item: barItemToggleDatePicker
•Activity Indicator View: activityIndicatorView
www.letsnurture.com
Step 2
As you see, we added some bar button items in our views. We require from
them to react on our taps, so we need to create IBAction methods to make
that happen.
Creating an IBAction method is almost the same as I previously
demonstrated. For the Post bar button item only, here is the procedure in
detail:
On either the Document Outline or directly on the view, right-click or control-
click on the bar button item.
On the black popup menu, under the Sent Actions section, click on the circle
next to the Selector option and keep the mouse button down.
Drag and drop on the Assistant Editor window
www.letsnurture.com
On the new window, add the post in the Name field and leave everything else
as it is. Click on Connect or hit the Return button on your keyboard.
Now, follow the same pattern and create the next IBAction methods for the
given subviews:
•Sign out bar button item: revokeAccess
•Okay bar button item on the Input Accessory View Toolbar:
acceptEditingEvent
•Cancel bar button item on the Input Accessory View Toolbar:
cancelEditingEvent
•Okay bar button item on the Date Picker View Toolbar: acceptSelectedDate
•Cancel bar button item on the Date Picker View Toolbar: cancelPickingDate
•All-day bar button item on the Date Picker View Toolbar: toggleDatePicker
www.letsnurture.com
After the addition of all the IBOutlet properties and all the IBAction methods,
your ViewController.h file should look like this:
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITableView *tblPostData;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *barItemPost;
@property (weak, nonatomic) IBOutlet UIBarButtonItem
*barItemRevokeAccess;
@property (strong, nonatomic) IBOutlet UIToolbar
*toolbarInputAccessoryView;
@property (strong, nonatomic) IBOutlet UIView *viewDatePicker;
@property (weak, nonatomic) IBOutlet UIDatePicker *dpDatePicker;
@property (weak, nonatomic) IBOutlet UIBarButtonItem
*barItemToggleDatePicker;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView
*activityIndicatorView;
- (IBAction)post:(id)sender;
- (IBAction)revokeAccess:(id)sender;
- (IBAction)acceptEditingEvent:(id)sender;
- (IBAction)cancelEditingEvent:(id)sender;
- (IBAction)acceptSelectedDate:(id)sender;
- (IBAction)cancelPickingDate:(id)sender;
- (IBAction)toggleDatePicker:(id)sender;
@end
www.letsnurture.com
4. Adopting Protocols
Step 1
Now that the interface has been setup and configured
and the IBOutlet properties along with the IBAction
methods have been created and connected, it's time to
start writing some code.
Previously we added the GoogleOAuth header and
implementation files in the project, but that's not
enough to make our class work. We also need to
import it in the view controller's class and adopt its
protocol.
Open the ViewController.h file and import the
GoogleOAuth.h file at the top of the file:
www.letsnurture.com
Step 2
In addition to the GoogleOAuth class' protocol, we need to
adopt the following as well:
UITableViewDelegate: the delegate of the table view.
UITableViewDatasource: the datasource of the table view.
UITextFieldDelegate: the delegate of the textfield that will be
used to edit the event description.
So, while still within the ViewController.h file, modify the
@interface statement like this:
@interface ViewController : UIViewController <UITableViewDelegate,
UITableViewDataSource, UITextFieldDelegate, GoogleOAuthDelegate>
www.letsnurture.com
5. Declaring Private Properties & Methods
Step 1
There are some properties and some methods that should
be declared at the private section of the class and are
required to make everything work smoothly.
These properties are mostly going to store application
information, but some simple flags will also be used to
indicate program state.
The next code snippet presents all the private data
members you should add in your project (copy and paste
them if you'd like). The comments explain everything. Don't
forget that we are working now in the ViewController.m file!
www.letsnurture.com
@interface ViewController ()
// The string that contains the event description.
// Its value is set every time the event description gets edited and its
// value is displayed on the table view.
@property (nonatomic, strong) NSString *strEvent;
// The string that contains the date of the event.
// This is the value that is displayed on the table view.
@property (nonatomic, strong) NSString *strEventDate;
// This string is composed right before posting the event on the calendar.
// It's actually the quick-add string and contains the date data as well.
@property (nonatomic, strong) NSString *strEventTextToPost;
// The selected event date from the date picker.
@property (nonatomic, strong) NSDate *dtEvent;
// The textfield that is appeared on the table view for editing the event description.
@property (nonatomic, strong) UITextField *txtEvent;
www.letsnurture.com
// This array is one of the most important properties, as it contains
// all the calendars as NSDictionary objects.
@property (nonatomic, strong) NSMutableArray *arrGoogleCalendars;
 
// This dictionary contains the currently selected calendar.
// It's the one that appears on the table view when the calendar list
// is collapsed.
@property (nonatomic, strong) NSDictionary *dictCurrentCalendar;
 
// A GoogleOAuth object that handles everything regarding the Google.
@property (nonatomic, strong) GoogleOAuth *googleOAuth;
 
// This flag indicates whether the event description is being edited or not.
@property (nonatomic) BOOL isEditingEvent;
 
// It indicates whether the event is a full-day one.
@property (nonatomic) BOOL isFullDayEvent;
 
// It simply indicates whether the calendar list is expanded or not on the table
view.
@property (nonatomic) BOOL isCalendarListExpanded;
 
@end
www.letsnurture.com
Beyond that, add the next method declarations after 
the properties and before the @end statement:
-(void)setupEventTextfield;
-(NSString *)getStringFromDate:(NSDate *)date;
-(void)showOrHideActivityIndicatorView;
Step 2
After having declared all these properties, let's do 
some initialization. This will take place on the 
viewDidLoad: method. Note that in this method we 
also set self as the delegate and the datasource of 
the table view.
www.letsnurture.com
- (void)viewDidLoad
{
    [super viewDidLoad];
     
    // Set self as the delegate and datasource of the table view.
    [_tblPostData setDelegate:self];
    [_tblPostData setDataSource:self];
     
    // Set the initial values of the following private properties.
    _strEvent = @"";
    _strEventDate = @"Pick a date...";
    _isEditingEvent = NO;
    _isFullDayEvent = NO;
    _isCalendarListExpanded = NO;
     
    // Initialize the googleOAuth object.
    // Pay attention so as to initialize it with the initWithFrame: method, 
not just init.
    _googleOAuth = [[GoogleOAuth alloc] initWithFrame:self.view.frame];
    // Set self as the delegate.
    [_googleOAuth setGOAuthDelegate:self];
}
www.letsnurture.com
6. Table View Datasource & Delegate Methods
Let's keep going by writing some datasource and delegate methods 
related to the table view. 
For the time being, we'll add only some standard code. As we move on, 
we'll add even more code when it's required. So, let's go ahead. As I 
said at the beginning, there are going to be three sections on the table 
view:
(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 3;
}
Each section is going to contain one row. For the third section, we'll 
have as many rows as the calendars support. 
In this method, you can see how the _isCalendarListExpanded flag is 
used for first time:
www.letsnurture.com
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
(NSInteger)section{    
    if (section != 2) {
        return 1;
    }
    else{
        // Depending on whether the calendars are listed in the table view,
        // the respective section will have either one row, or as many as the 
calendars are.
        if (!_isCalendarListExpanded) {
            return 1;
        }
        else{
            return [_arrGoogleCalendars count];
        }
    }
}
www.letsnurture.com
Let's add some footer titles, in order to make our demo app more descriptive. 
Nothing hard here:
-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:
(NSInteger)section
{
    // Set the footer title depending on the section value.
    NSString *footerTitle = @"";
    if (section == 0) {
        footerTitle = @"Event short description";
    }
    else if (section == 1){
        footerTitle = @"Event date";
    }
    else{
        footerTitle = @"Google Calendar";
    }
     
    return footerTitle;
}
www.letsnurture.com
Set the height of the row for each cell:
-(CGFloat)tableView:(UITableView *)tableView 
heightForRowAtIndexPath:(NSIndexPath 
*)indexPath{
    return 50.0;
}
Regarding the tableView:cellForRowAtIndexPath: 
datasource method, this one is going to be built 
step-by-step while we add more functionalities to 
the app. For now, let's write only the basics:
www.letsnurture.com
-(UITableViewCell *)tableView:(UITableView *)tableView 
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView 
dequeueReusableCellWithIdentifier: CellIdentifier];
     
    if (cell == nil) {
        cell = [[UITableViewCell alloc] 
initWithStyle:UITableViewCellStyleDefault 
reuseIdentifier:CellIdentifier];
         
        [cell setSelectionStyle:UITableViewCellSelectionStyleGray];
        [cell setAccessoryType:UITableViewCellAccessoryNone];
      
        // Set a font for the cell textLabel.
        [[cell textLabel] setFont:[UIFont fontWithName:@"Trebuchet 
MS" size:15.0]];
    }
         
    return cell;
}
www.letsnurture.com
Finally, we have only one delegate method, the well-known 
tableView:didSelectRowAtIndexPath:. 
Just like the previous one, this is also going to be built step-by-step. For 
now, make it only remove the selection from each row that is tapped:
-(void)tableView:(UITableView *)tableView 
didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    // At first, remove the selection from the tapped cell.
    [[_tblPostData cellForRowAtIndexPath:indexPath] 
setSelected:NO];
}
www.letsnurture.com
7. Editing the Event Description
Step 1
Until now, I have already said many times that a textfield is going to appear on 
the table view every time that we want to change the event description. 
However, when not editing, the textfield should not appear. After we finish 
editing, we'll update the event description and make the textfield go away.
Before we bring this behavior to life, it might be better to implement the private 
method that we have declared, the setupEventTextfield. In this method, we'll do 
the following tasks:
• We will initialize the textfield by setting a frame related to the cell 
content view's frame and set a style too.
• We'll set the contents of the strEvent string as its text.
• Remember the (alone) toolbar we added in the Interface Builder 
earlier? We'll set it as the input accessory view of the textfield.
• We'll set self as its delegate so we can handle the Return key of the 
keyboard.
www.letsnurture.com
-(void)setupEventTextfield{
    // Initialize the textfield by setting the following properties.
    // Add or remove properties depending on your demand.
    if (!_txtEvent) {
        _txtEvent = [[UITextField alloc] 
initWithFrame:CGRectMake(10.0, 10.0,
                                                                  [[_tblPostData 
cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 
inSection:0]] contentView].frame.size.width - 20.0,
                                                                  30.0)];
        [_txtEvent setBorderStyle:UITextBorderStyleRoundedRect];
        [_txtEvent setText:_strEvent];
        [_txtEvent 
setInputAccessoryView:_toolbarInputAccessoryView];
        [_txtEvent setDelegate:self];
    }
}
www.letsnurture.com
Step 2
Now that this method is ready and we can call it any 
time we'd like to initialize the textfield, let's see how we 
can implement the behavior we expect from the table 
view. 
We want to display the textfield every time that we tap 
on the row of the first section, so let's do so.
Inside the tableView:didSelectRowAtIndexPath: we'll 
check if the event is currently being edited. 
If not, then we'll call the previously implemented method 
to initialize the textfield, we'll change the flag status 
indicating whether the event is being edited and we'll 
show the keyboard.
www.letsnurture.com
-(void)tableView:(UITableView *)tableView 
didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    // At first, remove the selection from the tapped cell.
    [[_tblPostData cellForRowAtIndexPath:indexPath] 
setSelected:NO];
     
    if ([indexPath section] == 0) {
        // If the row of the first section is tapped, check whether 
the event description is being edited or not.
        // If not, then setup and show the textfield on the cell.
        if (!_isEditingEvent) {
            [self setupEventTextfield];
        }
        else{
            return;
        }
         
        
www.letsnurture.com
// Change the value of the isEditingEvent flag.
        _isEditingEvent = !_isEditingEvent;
        // Reload the selected row.
        [_tblPostData reloadRowsAtIndexPaths:[NSArray 
arrayWithObject:indexPath]
                            
withRowAnimation:UITableViewRowAnimationAutomatic];
         
        // If the textfield has been added as a subview to the 
cell,
        // then make it the first responder and show the 
keyboard.
        if (_isEditingEvent) {
            [_txtEvent becomeFirstResponder];
        }
    }    
}
www.letsnurture.com
Step 3
Now let's update the tableView:cellForRowAtIndexPath: so it reflects the state of 
the first section at any given time:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:
(NSIndexPath *)indexPath{
    ...
    ...
    ...
    if ([indexPath section] == 0) {
        if (!_isEditingEvent) {
            // If currently the event description is not being edited then just show
            // the value of the strEvent string and let the cell contain a disclosure indicator accessory
view.
            // Also, set the gray as the selection style.
            [[cell textLabel] setText:_strEvent];
            [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
            [cell setSelectionStyle:UITableViewCellSelectionStyleGray];
        }
        else{
            // If the event description is being edited, then empty the textLabel text so as to avoid
            // having text behind the textfield.
            // Add the textfield as a subview to the cell's content view and turn the selection style to
none.
            [[cell textLabel] setText:@""];
            [[cell contentView] addSubview:_txtEvent];
            [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
        }
    }
}
www.letsnurture.com
Great! If you run the app now, you'll notice that the textfield 
appears on the table view when you tap on the row of the 
first section. However, you cannot make the textfield 
vanish and keep the edited value. Why? Because we need 
to implement the related IBAction methods of the input 
accessory view bar button items.
Step 4
Let's begin with the acceptEditingEvent: IBAction method. 
In short, in this method we'll keep the typed event 
description on the strEvent string, we'll change the 
isEditingEvent flag's value, we'll make the keyboard 
disappear, and we'll refresh the table view.
www.letsnurture.com
- (IBAction)acceptEditingEvent:(id)sender {
    // If the strEvent property is already initialized then set its value to nil
    // as it's going to be re-allocated right after.
    if (_strEvent) {
        _strEvent = nil;
    }
     
    // Keep the text entered in the textfield.
    _strEvent = [[NSString alloc] initWithString:[_txtEvent text]];
 
    // Indicate that no longer the event description is being edited.
    _isEditingEvent = NO;
     
    // Resign the first responder and make the textfield nil.
    [_txtEvent resignFirstResponder];
    [_txtEvent removeFromSuperview];
    _txtEvent = nil;
 
    // Reload the row of the first section of the table view.
    [_tblPostData reloadRowsAtIndexPaths:[NSArray arrayWithObject:
[NSIndexPath indexPathForRow:0 inSection:0]]
                        withRowAnimation:UITableViewRowAnimationAutomatic];
}
www.letsnurture.com
The cancelEditingEvent: is similar.
- (IBAction)cancelEditingEvent:(id)sender {
    // Indicate that no longer the event description is being 
edited.
    _isEditingEvent = NO;
     
    // Resign the first responder.
    [_txtEvent resignFirstResponder];
    [_txtEvent removeFromSuperview];
    _txtEvent = nil;
     
    // Reload the first row of the first section of the table view.
    [_tblPostData reloadRowsAtIndexPaths:[NSArray 
arrayWithObject:[NSIndexPath indexPathForRow:0 
inSection:0]]
                        
withRowAnimation:UITableViewRowAnimationAutomatic];
}
www.letsnurture.com
Step 5
In order to be perfectly complete regarding the textfield and the 
event description editing in general, we have one more thing left to 
do. Namely, we must handle the Return button of the keyboard and 
make it work just like the acceptEditingEvent: IBAction method. 
For this reason, implement the following delegate method:
-(BOOL)textFieldShouldReturn:(UITextField *)textField{
    // In case the Return button on the keyboard is tapped, 
call the acceptEditingEvent: method
    // to handle it.
    [self acceptEditingEvent:nil];
    return YES;
}
As you can see, we simply make a call to the IBAction method and 
nothing more. Now, we are 100% complete regarding the event 
editing and the textfield manipulation.
www.letsnurture.com
8. Picking an Event Date
Step 1
One part of our demo app is complete. Let's go ahead now 
and let's fully implement the date picking feature. 
In this case, what we want is when we tap on the row of the 
second section on the table view, to show the view that 
contains the date picker and through it to select a date. 
After that, we want the picked date to be displayed on the 
table view as well. Special care should be given to the fact 
that an event can be set for a specific time, but it can also 
be an all-day event, which means that picking a time has 
no point at all. We'll see all this next.
www.letsnurture.com
Let's begin by showing the date picker container 
view to the self.view. It's just a matter of one line, 
which should be written in the 
tableView:didSelectRowAtIndexPath: delegate 
method, under any other content it has until now:
-(void)tableView:(UITableView *)tableView 
didSelectRowAtIndexPath:(NSIndexPath 
*)indexPath{
    ...
    ...
    else if ([indexPath section] == 1){
        // If the row of the second section is tapped, 
just show the view that contains the date picker.
        [self.view addSubview:_viewDatePicker];
    }
}
www.letsnurture.com
Step 2
Okay, that was the easy part. If you run the app now and you tap on the 
row of the second section, the date picker container view will be displayed 
on the screen. 
You'll notice that by default, both date and time are represented in the 
picker. 
That's nice if we want to set a specific time for the event. However, we 
should not let the time appear on the date picker if we are talking about 
an all-day event. Therefore, we need to implement the toggleDatePicker: 
IBAction method.
The way this method is going to work is fairly simple. Depending on the 
current date picker's contents, we will set its mode and we'll also change 
the respective bar button item's title. 
Don't forget that there is a flag as well, the isFullDayEvent variable, that 
should be changed accordingly. The following is the implementation:
www.letsnurture.com
- (IBAction)toggleDatePicker:(id)sender {
    if ([_dpDatePicker datePickerMode] == UIDatePickerModeDateAndTime) 
{
        // If the date picker currently shows both date and time, then set it to 
show only date
        // and change the title of the barItemToggleDatePicker item.
        // In this case the user selects to make a full-day event.
        [_dpDatePicker setDatePickerMode:UIDatePickerModeDate];
        [_barItemToggleDatePicker setTitle:@"Specific time"];
    }
    else{
        // Otherwise, if only date is shown on the date picker, set it to show time 
too.
        // The event is no longer a full-day one.
        [_dpDatePicker setDatePickerMode:UIDatePickerModeDateAndTime];
        [_barItemToggleDatePicker setTitle:@"All-day event"];
    }
     
    // Change the flag that indicates whether is a full-day event or not.
    _isFullDayEvent = !_isFullDayEvent;
}
www.letsnurture.com
Give it another try now and tap (or click on the Simulator) 
on the All-day event bar button item. Notice how the 
contents of the date picker get changed, along with the title 
of the button.
Step 3
Now we know when an event is set as a full-day event, but 
we are still unable to keep the selected date and to make it 
appear on the table view. Similarly, we cannot yet cancel 
the date picking and go back to our view.
So, let's work on both of these IBAction methods now.
In the acceptSelectedDate: IBAction method we need to do 
only four things: (1) keep the selected date as a string, (2) 
store the date as a NSDate object, (3) remove the view 
from the superview, and, finally, (4) reload our table view in 
order to show the selected date.
www.letsnurture.com
- (IBAction)acceptSelectedDate:(id)sender
{
// Keep the selected date as a NSDate object.
_dtEvent = [_dpDatePicker date];
// Also, convert it to a string properly formatted depending on
whether the event is a full-day one or not
// by calling the getStringFromDate: method.
_strEventDate = [[NSString alloc] initWithString:[self
getStringFromDate:[_dpDatePicker date]]];
// Remove the view with the date picker from the self.view.
[_viewDatePicker removeFromSuperview];
// Reload the row of the second section of the table view to reflect
the selected date.
[_tblPostData reloadRowsAtIndexPaths:[NSArray arrayWithObject:
[NSIndexPath indexPathForRow:0 inSection:1]]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
www.letsnurture.com
In the cancelPickingDate: IBAction method, we only
need to remove the date picker container view from
the superview.
- (IBAction)cancelPickingDate:(id)sender {
// Just remove the view with the date picker from
the superview.
[_viewDatePicker removeFromSuperview];
}
www.letsnurture.com
Step 4
In the acceptSelectedDate: method, we made a call to
the getStringFromDate: private method, which is
declared but not yet implemented. It's now time to work
with this method. Before I present the code, I should
make an observation.
The purpose of the getStringFromDate: method is to get
the date we provide (the selected date in our example)
and to return this date as a string and formatted the way
we want. However, there are two kind of string formats
we need to have, depending on whether the event is a
full-day one or not. If the event date has a specific time,
we want this time to be present on the string. This
means that we'll have a condition in our method that will
determine what the output string format will be. So,
having made our intention clear, enter the following
code:
www.letsnurture.com
-(NSString *)getStringFromDate:(NSDate *)date{
// Create a NSDateFormatter object to handle the date.
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
if (!_isFullDayEvent) {
// If it's not a full-day event, then set the date format in a way that
contains the time too.
[formatter setDateFormat:@"EEE, MMM dd, yyyy, HH:mm"];
}
else{
// Otherwise keep just the date.
[formatter setDateFormat:@"EEE, MMM dd, yyyy"];
}
// Return the formatted date as a string value.
return [formatter stringFromDate:date];
}
www.letsnurture.com
The above will give us something like Mon, Aug 12, 2013,
17:32.
For more information about the date symbols used here
and any other symbols that exist, look at the
Date Format Patterns.
Note: Keep in mind that you should change the date
symbol order in a real app in order to match the date
representation of your own country.
www.letsnurture.com
Step 5
So far so good: just one thing left to do. Add some code on the
tableView:cellForRowAtIndexPath: method so the selected date will be
displayed. Under all of the other contents of the method, add the next
snippet:
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
...
...
...
else if ([indexPath section] == 1){
// In the event date cell just show the strEventDate string which
either prompts the user
// to pick a date, or contains the selected date as a string.
// Also, add a disclosure indicator view.
[[cell textLabel] setText:_strEventDate];
[cell
setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
}
return cell;
}
www.letsnurture.com
9. App Authorization
If you have already run the app, or if you do it now, you'll notice that the row
of the third section on the table view contains the Download calendars...
message.
Of course, nothing takes place when you tap on this method.
What we'd like to do on tap is to make an API call to Google and request
the information we need. But, prior to this, we must authorize ourselves
against the
Google service and obtain an access token that will be used to exchange
data.
Actually, all of this will be done by the GoogleOAuth class we included early
on.
All wee need to do is provide this class with the client ID, the client secret,
and the scope. So, let's pay a visit to the Google developers website and
get all the values we need.
www.letsnurture.com
Step 1
Go to the Google Developer website. Click on the Sign In button that
exists on the top-right side of the webpage to login.
www.letsnurture.com
After you have signed in, scroll down on the page until you locate the API
Console icon.
Click on it and you'll be transferred to your Dashboard, where you handle all
of your projects.
If you must, create a project now, otherwise select the project you created
from the previous tutorial in this series. On the menu at the left side of the
webpage, click on the API Access option.
www.letsnurture.com
Details about the current project will be displayed on the right side of the
page.
In there, you can track down the client ID and client secret values that
you will need. Note them, and let's move ahead.
www.letsnurture.com
Next, we need to tell Google that we want to use the Calendars service. To
do so, click on the Services option at the menu on the left side of the
webpage.
A list of all the provided services will appear. Locate the Calendar API item.
Click on the Off button to enable the specific service for the current project.
www.letsnurture.com
Step 2
What we haven't located yet is the scope value for getting the calendar info we need.
Just to remind you, a scope indicates an API that the app requests access for.
The best practice to locate what you want is to use the search engine for the Google
developer website. So, go to the homepage of the Google developer site and search
for the term Google Calendar API. In the results page, click on the first result.
ake a look around if you want and explore using the menus. For this tutorial,
you should go to Reference > Calendar List > list.
A new webpage is loaded, containing information about the calendar list API
call.
Find the Authorization area near the bottom of the page. There you can find
the scope we need: https://www.googleapis.com/auth/calendar.
www.letsnurture.com
Step 3
In the previous steps, I showed you how you can
enable a service on Google and the best way to find
what you need from the developer web site.
Now, we can keep building the app.
Now that we have access to all the data we need
from Google, let's get back to our app and see how
to use the information.
Append the following code to our project:
www.letsnurture.com
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
(NSIndexPath *)indexPath{
...
...
...
else if ([indexPath section] == 2){
if (_arrGoogleCalendars == nil || [_arrGoogleCalendars count] == 0)
{
// If the arrGoogleCalendars array is nil or contains nothing, then
the calendars should be
// downloaded from Google.
// So, show the activity indicator view and authorize the user by
calling the the next
// method of our custom-made class.
[self showOrHideActivityIndicatorView];
[_googleOAuth authorizeUserWithClienID:@"YOUR_CLIENT_ID"
andClientSecret:@"YOUR_CLIENT_SECRET"
andParentView:self.view
andScopes:[NSArray
arrayWithObject:@"https://www.googleapis.com/auth/calendar"]];
}
}
www.letsnurture.com
You'll notice that above we display the indicator view as
well.
We do this because we don't know how long it'll take to
obtain authorization. Don't forget to set your own values for
the client ID and the client secret!
When you use the app for the first time, the embedded
web view will appear.
You must enter your credentials and sign into your Google
account to allow the app to access your calendars. If
everything goes okay, you'll be authorized without a
problem.
www.letsnurture.com
10. Downloading Calendar Data
Step 1
At this point, we need to implement some of the
GoogleOAuth delegate methods.
Let's begin with the authorizationWasSuccessful method,
where we'll handle a successful authorization by making
the API call for getting the calendar list.
However, we need the API URL string if we want to
proceed. Navigate to the proper calendar list page in your
Google developer account. At the top of the page you'll find
the URL along with the HTTP method that should be used.
www.letsnurture.com
Now we can implement the delegate method:
-(void)authorizationWasSuccessful{
// If user authorization is successful, then make an API call to
get the calendar list.
// For more infomation about this API call, visit:
// https://developers.google.com/google-
apps/calendar/v3/reference/calendarList/list
[_googleOAuth
callAPI:@"https://www.googleapis.com/calendar/v3/users/me/cal
endarList"
withHttpMethod:httpMethod_GET
postParameterNames:nil
postParameterValues:nil];
}
www.letsnurture.com
Step 2
If all works according to plan, the
responseFromServiceWasReceived:andResponseJSONAsD
ata: delegate method will be called by the GoogleOAuth
class.
We are responsible to check if Google responded with the
desired results, and then to keep only the data that we care
about.
Let's discuss a bit about what the response contains and
how we are going to manage the data.
What we should do first is to convert the response JSON
data into an NSDictionary object. If you NSLog this
dictionary, we'll see the way the returned data is formed. See
the following example:
www.letsnurture.com
{
etag =
""AaJWGrGt8CrZSonQa3iAA4QAo_s/oZDiRXBv
AIXr3JkNwKQRZZfQzQ4"";
items = (
{ … }
{ … }
{ … }
);
kind = "calendar#calendarList";
}
www.letsnurture.com
Inside each curly bracket there is a block containing a
bunch of information regarding every calendar you have
created in Google Calendars.
The items object is equivalent to an array which contains
dictionaries as objects.
In other words, we will extract the items object as a
NSArray and we'll handle every single object of it as a
NSDictionary object.
www.letsnurture.com
Let's get back on track again. Once we acquire all the items
as NSArray objects, we'll go through a loop to access each
calendar's details and we'll keep only the values we want
for the purposes of this example.
Actually, we are going to create key-value pairs with these
values with the goal of creating new NSDictionaries, which
will be stored in the arrGoogleCalendars array. This array is
the calendar list for our app.
Also, the dictCurrentCalendar dictionary will be initialized
with the contents of the first calendar from the list. Once this
has been done, the Post and Logout items will become
enabled. We'll also hide the activity indicator view and we'll
refresh the table to show the selected calendar.
www.letsnurture.com
-(void)responseFromServiceWasReceived:(NSString *)responseJSONAsString
andResponseJSONAsData:(NSData *)responseJSONAsData{
NSError *error;
if ([responseJSONAsString rangeOfString:@"calendarList"].location !=
NSNotFound) {
// If the response from Google contains the "calendarList" literal, then the
calendar list
// has been downloaded.
// Get the JSON data as a dictionary.
NSDictionary *calendarInfoDict = [NSJSONSerialization
JSONObjectWithData:responseJSONAsData
options:NSJSONReadingMutableContainers error:&error];
if (error) {
// This is the case that an error occured during converting JSON data to
dictionary.
// Simply log the error description.
NSLog(@"%@", [error localizedDescription]);
}
www.letsnurture.com
else{
// Get the calendars info as an array.
NSArray *calendarsInfo = [calendarInfoDict
objectForKey:@"items"];
// If the arrGoogleCalendars array is nil then
initialize it so to store each calendar as a NSDictionary
object.
if (_arrGoogleCalendars == nil) {
_arrGoogleCalendars = [[NSMutableArray alloc]
init];
}
www.letsnurture.com
// Make a loop and get the next data of each calendar.
for (int i=0; i<[calendarsInfo count]; i++) {
// Store each calendar in a temporary dictionary.
NSDictionary *currentCalDict = [calendarsInfo objectAtIndex:i];
// Create an array which contains only the desired data.
NSArray *values = [NSArray arrayWithObjects:[currentCalDict
objectForKey:@"id"],
[currentCalDict objectForKey:@"summary"],
nil];
// Create an array with keys regarding the values on the previous
array.
NSArray *keys = [NSArray arrayWithObjects:@"id", @"summary",
nil];
// Add key-value pairs in a dictionary and then add this dictionary
into the arrGoogleCalendars array.
[_arrGoogleCalendars addObject:
[[NSMutableDictionary alloc] initWithObjects:values forKeys:keys]];
}
www.letsnurture.com
// Set the first calendar as the selected one.
_dictCurrentCalendar = [[NSDictionary alloc]
initWithDictionary:[_arrGoogleCalendars objectAtIndex:0]];
// Enable the post and the sign out bar button items.
[_barItemPost setEnabled:YES];
[_barItemRevokeAccess setEnabled:YES];
// Stop the activity indicator view.
[self showOrHideActivityIndicatorView];
// Reload the table view section.
[_tblPostData reloadRowsAtIndexPaths:[NSArray
arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:2]]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}
www.letsnurture.com
Step 3
Now that we've done all the above, we want to be able to
tap on a calendar name and have the full list expand,
allowing us to select another calendar.
This will be done in the
tableView:didSelectRowAtIndexPath: method. Add the
following code snippet:
www.letsnurture.com
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
(NSIndexPath *)indexPath{
...
else{
// In this case the calendars exist in the arrGoogleCalendars array.
if (_isCalendarListExpanded) {
// If the calendar list is shown on the table view, then the tapped one
shoule become the selected calendar.
// Re-initialize the dictCurrentCalendar dictionary so it contains the
information regarding the selected one.
_dictCurrentCalendar = nil;
_dictCurrentCalendar = [[NSDictionary alloc] initWithDictionary:
[_arrGoogleCalendars objectAtIndex:[indexPath row]]];
}
// Change the value of the isCalendarListExpanded which indicates
whether only the selected calendar is shown, or the
// whole list.
_isCalendarListExpanded = !_isCalendarListExpanded;
// Finally, reload the section.
[_tblPostData reloadSections:[NSIndexSet indexSetWithIndex:2]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
} }
www.letsnurture.com
Step 4
Finally, we need to update the tableView:cellForRowAtIndexPath: method to
display everything we've done. Append the following code:
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
...
...
...
else if ([indexPath section] == 2){
// This is the case where either the selected calendar is shown, or a list
with all of them.
if (!_isCalendarListExpanded) {
// If the calendar list is not expanded and only the selected calendar is
shown,
// then if the arrGoogleCalendars array is nil or it doesn't have any
contents at all prompt
// the user to download them now.
// Otherwise show the summary (title) of the selected calendar along
with a disclosure indicator.
if (![_arrGoogleCalendars count] || [_arrGoogleCalendars count] == 0)
{
[[cell textLabel] setText:@"Download calendars..."];
}
www.letsnurture.com
else{
[[cell textLabel] setText:[_dictCurrentCalendar
objectForKey:@"summary"]];
}
[cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
}
else{
// This is the case where all the calendars should be listed.
// Note that each calendar is represented as a NSDictionary which is read from
the // arrGoogleCalendars array.
// If the calendar that is shown in the current cell is the already selected one,
// then add the checkmark accessory type to the cell, otherwise set the
accessory type to none.
NSDictionary *tempDict = [_arrGoogleCalendars objectAtIndex:
[indexPath row]];
[[cell textLabel] setText:[tempDict objectForKey:@"summary"]];
if ([tempDict isEqual:_dictCurrentCalendar]) {
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
}
else{
[cell setAccessoryType:UITableViewCellAccessoryNone];
} } }
return cell; }
www.letsnurture.com
That's it. Go and give it a try. Watch your calendars on the
Simulator and play around for a while by expanding the
calendar list and selecting a calendar!
11. Posting Calendar Events
Step 1
Let's see how we'll manage to add an event to a selected
calendar. The first thing we should do is make sure that
the event description and the event date have been set. To
verify this, we'll check their values and we'll show an alert
if something is wrong.
Our work will now take place in the post: IBAction method.
www.letsnurture.com
- (IBAction)post:(id)sender {
// Before posting the event, check if the event description is empty or a date has
not been selected.
if ([_strEvent isEqualToString:@""]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@""
message:@"Please enter an event description."
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"Okay", nil];
[alert show];
return;
}
if ([_strEventDate isEqualToString:@"Pick a date..."]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@""
message:@"Please select a date for the event."
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"Okay", nil];
[alert show];
return;
}
...
}
www.letsnurture.com
You'll notice that we'll simply check if the event description string is equal
to the empty string and if the event date string is equal to its initial value.
Step 2
As I have already said, what we actually do in this project is to implement
the Quick-add feature that Google Calendar supports.
However, we need to know the format for the event string because there
are some rules that apply.
For example, there are special ways that the event date and time should
be appended at the end of the event description string, so Google will
know the exact date/time of the event.
Of course, if you have already used the online Quick-Add feature, then
you surely know what I am talking about.
www.letsnurture.com
Thankfully, Google provides help and examples regarding
this issue. We just have to visit the Quick Add
documentation. Go and visit this website and familiarize
your self with this feature.
In our case, if we have a full-day event, we simply have to
add the date using slashes at the end of the event
description string (for example, "This is an event
08/12/2013"). If we don't have a full-day event, then we
will add the time too, using the "at" between the date and
time (for example, "This is an event 08/12/2013 at 21:40").
In addition, we need to know the URL of the API we want
to call. Just like we with the calendar list, we must find the
quick-add related information in the Google Calendar API
documentation. If you don't want to bother looking for it
right now, this is where you can find it. We must search
for the following information:
www.letsnurture.com
Request: This is the URL of the API we want to call as it
appeared at the top of the page.
Also notice that we need to use the POST HTTP method.
POST parameters:
Here are the parameters we need to send with the POST
method. There are only two mandatory params, the ID of
the calendar on which we want to add the event and the
event text (of course formatted with the date, as I indicated
before).
Authorization: This is the scope that we should be
authorizing. In this case, the scope is equal to the calendar
list scope, so we don't need to care about it. However, if
that was a different value, then we should include it in the
scope array during authorization.
www.letsnurture.com
- (IBAction)post:(id)sender {
...
...
...
// Create the URL string of API needed to quick-add the event into the
Google calendar.
// Note that we specify the id of the selected calendar.
NSString *apiURLString = [NSString
stringWithFormat:@"https://www.googleapis.com/calendar/v3/calendars/
%@/events/quickAdd",
[_dictCurrentCalendar objectForKey:@"id"]];
// Build the event text string, composed by the event description and
the date (and time) that should happen.
// Break the selected date into its components.
NSDateComponents *dateComponents = [[NSDateComponents alloc]
init];
dateComponents = [[NSCalendar currentCalendar]
components:NSDayCalendarUnit|NSMonthCalendarUnit|
NSYearCalendarUnit|NSHourCalendarUnit|NSMinuteCalendarUnit
fromDate:_dtEvent];
www.letsnurture.com
if (_isFullDayEvent) {
// If a full-day event was selected (meaning without specific
time), then add at the end of the string just the date.
_strEventTextToPost = [NSString stringWithFormat:@"%@
%d/%d/%d", _strEvent, [dateComponents month],
[dateComponents day], [dateComponents year]];
}
else{
// Otherwise, append both the date and the time that the
event should happen.
_strEventTextToPost = [NSString stringWithFormat:@"%@
%d/%d/%d at %d.%d", _strEvent, [dateComponents month],
[dateComponents day], [dateComponents year],
[dateComponents hour], [dateComponents minute]];
}
www.letsnurture.com
// Show the activity indicator view.
[self showOrHideActivityIndicatorView];
// Call the API and post the event on the selected Google
calendar.
// Visit https://developers.google.com/google-
apps/calendar/v3/reference/events/quickAdd for more
information about the quick-add event API call.
[_googleOAuth callAPI:apiURLString
withHttpMethod:httpMethod_POST
postParameterNames:[NSArray
arrayWithObjects:@"calendarId", @"text", nil]
postParameterValues:[NSArray arrayWithObjects:
[_dictCurrentCalendar objectForKey:@"id"],
_strEventTextToPost, nil]];
}
www.letsnurture.com
If you run the app now and you click on the Post button, the
event will be posted. You can verify this if you check your
calendar from a web browser. However, until we handle the
response from Google, we are unable to know if the event
was successfully added or not.
Step 3
Each time that an event is posted, Google creates a
respective object which contains several properties and
data.
After a successful addition, Google responds with this data,
so we can have any information we need regarding the
newly created event. In our case, we won't do something
extraordinary. We'll simply show an alert view that will
contain the following values, so we are sure that the event
was successfully posted.
www.letsnurture.com
ID: Each new event gets a unique ID value, which we are
going to display on the alert view.
Created Date: The date that the event was created.
Summary: The event description itself.
Of course, in a real app, you must handle this event data in
a different way. Always regard the app's needs.
We are going to work in the
responseFromServiceWasReceived:andResponseJSONAs
Data: method again. Notice that the JSON data is
converted again into an NSDictionary object.
www.letsnurture.com
-(void)responseFromServiceWasReceived:(NSString *)responseJSONAsString
andResponseJSONAsData:(NSData *)responseJSONAsData{
NSError *error;
...
...
...
else if ([responseJSONAsString rangeOfString:@"calendar#event"].location !
= NSNotFound){
// If the Google response contains the "calendar#event" literal then the
event has been added to the selected calendar
// and Google returns data related to the new event.
// Get the response JSON as a dictionary.
NSDictionary *eventInfoDict = [NSJSONSerialization
JSONObjectWithData:responseJSONAsData
options:NSJSONReadingMutableContainers error:&error];
if (error) {
// This is the case that an error occured during converting JSON data to
dictionary.
// Simply log the error description.
NSLog(@"%@", [error localizedDescription]);
return;
}
www.letsnurture.com
// An alert view with some information regarding the just added event will be
shown.
// Keep only the information that will be shown to the alert view.
// Look at the https://developers.google.com/google-
apps/calendar/v3/reference/events#resource for a complete list of the
// data fields that Google returns.
NSString *eventID = [eventInfoDict objectForKey:@"id"];
NSString *created = [eventInfoDict objectForKey:@"created"];
NSString *summary = [eventInfoDict objectForKey:@"summary"];
// Build the alert message.
NSString *alertMessage = [NSString stringWithFormat:@"ID:
%@nnCreated:%@nnSummary:%@", eventID, created, summary];
// Stop the activity indicator view.
[self showOrHideActivityIndicatorView];
// Show the alert view.
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"New event"
message:alertMessage
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"Great", nil];
[alert show];
}
}
www.letsnurture.com
Now, every time you add an event an alert view containing
the new event info is displayed.
12. Signing Out
Signing out of the Google account is an option that should
always be provided to the users, even though it's
recommended to keep them connected for faster access to
online services.
For our app, it's only a matter of one single line of code,
which we'll add on the revokeAccess: IBAction method.
www.letsnurture.com
13. Ancillary Methods
Step 1
Until now, we used only two delegate methods of the
GoogleOAuth class, the authorizationWasSuccessful and the
responseFromServiceWasReceived:andResponseJSONAsData:.
There are three more of them that we should implement. We need
a delegate method for handling the access revocation, another
delegate for handling any error that may occur, and a final method
for dealing with any error messages that may exist in Google
responses.
Regarding the access revocation, we'll have to do only three
things. First, we need to remove all calendars from the
arrGoogleCalendars array. Next, we need to disable the Post and
the Sign Out buttons. Finally, we need to reload the table view to
keep it up to date.
www.letsnurture.com
-(void)accessTokenWasRevoked{
// Remove all calendars from the array.
[_arrGoogleCalendars removeAllObjects];
_arrGoogleCalendars = nil;
// Disable the post and sign out bar button items.
[_barItemPost setEnabled:NO];
[_barItemRevokeAccess setEnabled:NO];
// Reload the Google calendars section.
[_tblPostData reloadSections:[NSIndexSet
indexSetWithIndex:2]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
www.letsnurture.com
For the next two error handling delegate methods, we won't do much. We'll
simply log the error messages and nothing further.
It's obvious that in a real app you would have to handle the errors in an
appropriate way and figure out workarounds that handle unexpected situations.
For now, here are our stub implementations:
-(void)errorOccuredWithShortDescription:(NSString
*)errorShortDescription andErrorDetails:(NSString
*)errorDetails{
// Just log the error messages.
NSLog(@"%@", errorShortDescription);
NSLog(@"%@", errorDetails);
}
-(void)errorInResponseWithBody:(NSString *)errorMessage{
// Just log the error message.
NSLog(@"%@", errorMessage);
}
www.letsnurture.com
Step 2
During the project implementation, we made a few calls to the
showOrHideActivityIndicatorView private method, which has been declared and
called but not yet built. Let's deal with it now.
-(void)showOrHideActivityIndicatorView{
// If the activity indicator view is not currently animating (spinning),
// then set its view center equal to self view's center, add it as a subview and
start animating.
// Otherwise stop animating and remove it from the superview.
if (![_activityIndicatorView isAnimating]) {
[_activityIndicatorView setCenter:self.view.center];
[self.view addSubview:_activityIndicatorView];
[_activityIndicatorView startAnimating];
}
else{
[_activityIndicatorView stopAnimating];
[_activityIndicatorView removeFromSuperview];
}
}
Follow us on
https://www.facebook.com/LetsNurture
https://twitter.com/letsnurture
http://www.linkedin.com/company/letsnurture
info@letsnurture.om
www.letsnurture.com | www.letsnurture.co.uk

More Related Content

What's hot

What's hot (18)

Cis407 a ilab 1 web application development devry university
Cis407 a ilab 1 web application development devry universityCis407 a ilab 1 web application development devry university
Cis407 a ilab 1 web application development devry university
 
Primavera manual tutorial book
Primavera manual tutorial bookPrimavera manual tutorial book
Primavera manual tutorial book
 
News and Events Tutorial
News and Events TutorialNews and Events Tutorial
News and Events Tutorial
 
leggi
leggileggi
leggi
 
Get started with watch kit development
Get started with watch kit developmentGet started with watch kit development
Get started with watch kit development
 
Places Tutorial
Places TutorialPlaces Tutorial
Places Tutorial
 
Programming Without Coding Technology (PWCT) - Add toolbar to the window
Programming Without Coding Technology (PWCT) - Add toolbar to the windowProgramming Without Coding Technology (PWCT) - Add toolbar to the window
Programming Without Coding Technology (PWCT) - Add toolbar to the window
 
TapTimeDocumentation
TapTimeDocumentationTapTimeDocumentation
TapTimeDocumentation
 
Step-by-Step - Windows 7
Step-by-Step - Windows 7Step-by-Step - Windows 7
Step-by-Step - Windows 7
 
Expense personalization
Expense personalizationExpense personalization
Expense personalization
 
Talk tomepart1
Talk tomepart1Talk tomepart1
Talk tomepart1
 
Loyalty Stamp Tutorial
Loyalty Stamp TutorialLoyalty Stamp Tutorial
Loyalty Stamp Tutorial
 
Social Wall Tutorial
Social Wall TutorialSocial Wall Tutorial
Social Wall Tutorial
 
67
6767
67
 
Hpalm
HpalmHpalm
Hpalm
 
Meet the New Microsoft Windows 8 - User Guide
Meet the New Microsoft Windows 8 - User GuideMeet the New Microsoft Windows 8 - User Guide
Meet the New Microsoft Windows 8 - User Guide
 
Gokul bok
Gokul bokGokul bok
Gokul bok
 
Point&amp;Click
Point&amp;ClickPoint&amp;Click
Point&amp;Click
 

Viewers also liked

Program pelerinaj Israel
Program pelerinaj IsraelProgram pelerinaj Israel
Program pelerinaj IsraelCelina Dumitru
 
Facebook twitter-promo-extenstion-installation
Facebook twitter-promo-extenstion-installationFacebook twitter-promo-extenstion-installation
Facebook twitter-promo-extenstion-installationKetan Raval
 
فئات خاصة
فئات خاصةفئات خاصة
فئات خاصةd-norah
 
World healthcare conference madu-v3
World healthcare conference madu-v3World healthcare conference madu-v3
World healthcare conference madu-v3caribbeanheart
 
Saptamana spiritualitatii 2012
Saptamana spiritualitatii 2012Saptamana spiritualitatii 2012
Saptamana spiritualitatii 2012Celina Dumitru
 
Local Notification Tutorial
Local Notification TutorialLocal Notification Tutorial
Local Notification TutorialKetan Raval
 
классицизм в архитектуре санкт петербурга болонова №2
классицизм в архитектуре санкт петербурга болонова №2классицизм в архитектуре санкт петербурга болонова №2
классицизм в архитектуре санкт петербурга болонова №2kryljanauki
 
Introduction to-concrete-5
Introduction to-concrete-5Introduction to-concrete-5
Introduction to-concrete-5Ketan Raval
 
Marshall Cassidy : VOCALSpin : My Dream Girl
Marshall Cassidy : VOCALSpin : My Dream GirlMarshall Cassidy : VOCALSpin : My Dream Girl
Marshall Cassidy : VOCALSpin : My Dream GirlVOCAL SPIN
 
Marshall Cassidy : VOCALSpin : Horse Around - The Saratoga Song
Marshall Cassidy : VOCALSpin : Horse Around - The Saratoga SongMarshall Cassidy : VOCALSpin : Horse Around - The Saratoga Song
Marshall Cassidy : VOCALSpin : Horse Around - The Saratoga SongVOCAL SPIN
 
How to manage cms pages in Magento
How to manage cms pages in MagentoHow to manage cms pages in Magento
How to manage cms pages in MagentoKetan Raval
 
классицизм в живописи
классицизм в живописиклассицизм в живописи
классицизм в живописиkryljanauki
 
Feasibility study
Feasibility  studyFeasibility  study
Feasibility studyjilly17
 
Android notifications
Android notificationsAndroid notifications
Android notificationsKetan Raval
 
โลกสอนฉัน
โลกสอนฉันโลกสอนฉัน
โลกสอนฉันNa Tak
 
Latest adwords innovations
Latest adwords innovationsLatest adwords innovations
Latest adwords innovationsKetan Raval
 
Holes (themes)
Holes (themes)Holes (themes)
Holes (themes)salmaowen
 

Viewers also liked (20)

Curriculum ppt
Curriculum pptCurriculum ppt
Curriculum ppt
 
Program pelerinaj Israel
Program pelerinaj IsraelProgram pelerinaj Israel
Program pelerinaj Israel
 
Facebook twitter-promo-extenstion-installation
Facebook twitter-promo-extenstion-installationFacebook twitter-promo-extenstion-installation
Facebook twitter-promo-extenstion-installation
 
Assignment brief
Assignment briefAssignment brief
Assignment brief
 
فئات خاصة
فئات خاصةفئات خاصة
فئات خاصة
 
World healthcare conference madu-v3
World healthcare conference madu-v3World healthcare conference madu-v3
World healthcare conference madu-v3
 
Saptamana spiritualitatii 2012
Saptamana spiritualitatii 2012Saptamana spiritualitatii 2012
Saptamana spiritualitatii 2012
 
Local Notification Tutorial
Local Notification TutorialLocal Notification Tutorial
Local Notification Tutorial
 
классицизм в архитектуре санкт петербурга болонова №2
классицизм в архитектуре санкт петербурга болонова №2классицизм в архитектуре санкт петербурга болонова №2
классицизм в архитектуре санкт петербурга болонова №2
 
Introduction to-concrete-5
Introduction to-concrete-5Introduction to-concrete-5
Introduction to-concrete-5
 
Marshall Cassidy : VOCALSpin : My Dream Girl
Marshall Cassidy : VOCALSpin : My Dream GirlMarshall Cassidy : VOCALSpin : My Dream Girl
Marshall Cassidy : VOCALSpin : My Dream Girl
 
Marshall Cassidy : VOCALSpin : Horse Around - The Saratoga Song
Marshall Cassidy : VOCALSpin : Horse Around - The Saratoga SongMarshall Cassidy : VOCALSpin : Horse Around - The Saratoga Song
Marshall Cassidy : VOCALSpin : Horse Around - The Saratoga Song
 
How to manage cms pages in Magento
How to manage cms pages in MagentoHow to manage cms pages in Magento
How to manage cms pages in Magento
 
классицизм в живописи
классицизм в живописиклассицизм в живописи
классицизм в живописи
 
Feasibility study
Feasibility  studyFeasibility  study
Feasibility study
 
Android notifications
Android notificationsAndroid notifications
Android notifications
 
โลกสอนฉัน
โลกสอนฉันโลกสอนฉัน
โลกสอนฉัน
 
Latest adwords innovations
Latest adwords innovationsLatest adwords innovations
Latest adwords innovations
 
Vyukova aktivita
Vyukova aktivitaVyukova aktivita
Vyukova aktivita
 
Holes (themes)
Holes (themes)Holes (themes)
Holes (themes)
 

Similar to Google calendar integration in iOS app

Day to day use of Logical Triggers.pdf
Day to day use of Logical Triggers.pdfDay to day use of Logical Triggers.pdf
Day to day use of Logical Triggers.pdfHasseyWijetunge
 
Guide to Fix Dropdown Button Not Switching Selected Item | Flutter
Guide to Fix Dropdown Button Not Switching Selected Item | FlutterGuide to Fix Dropdown Button Not Switching Selected Item | Flutter
Guide to Fix Dropdown Button Not Switching Selected Item | FlutterFlutter Agency
 
Getting Started with Developing for the Apple Watch
Getting Started with Developing for the Apple WatchGetting Started with Developing for the Apple Watch
Getting Started with Developing for the Apple WatchMurtza Manzur
 
Software engineering modeling lab lectures
Software engineering modeling lab lecturesSoftware engineering modeling lab lectures
Software engineering modeling lab lecturesmarwaeng
 
Information about Toggle Button.pdf
Information about Toggle Button.pdfInformation about Toggle Button.pdf
Information about Toggle Button.pdfNishaadequateinfosof
 
Gui in matlab :
Gui in matlab :Gui in matlab :
Gui in matlab :elboob2025
 
Android User Interface: Basic Form Widgets
Android User Interface: Basic Form WidgetsAndroid User Interface: Basic Form Widgets
Android User Interface: Basic Form WidgetsAhsanul Karim
 
Ios actions and outlets
Ios actions and outletsIos actions and outlets
Ios actions and outletsveeracynixit
 
Ios actions and outlets
Ios actions and outletsIos actions and outlets
Ios actions and outletsveeracynixit
 
Toolbar, statusbar, coolbar in vb
Toolbar, statusbar, coolbar in vbToolbar, statusbar, coolbar in vb
Toolbar, statusbar, coolbar in vbAmandeep Kaur
 
Gui builder
Gui builderGui builder
Gui builderlearnt
 
Using prime[31] to connect your unity game to azure mobile services
Using prime[31] to connect your unity game to azure mobile servicesUsing prime[31] to connect your unity game to azure mobile services
Using prime[31] to connect your unity game to azure mobile servicesDavid Voyles
 

Similar to Google calendar integration in iOS app (20)

Day to day use of Logical Triggers.pdf
Day to day use of Logical Triggers.pdfDay to day use of Logical Triggers.pdf
Day to day use of Logical Triggers.pdf
 
Guide to Fix Dropdown Button Not Switching Selected Item | Flutter
Guide to Fix Dropdown Button Not Switching Selected Item | FlutterGuide to Fix Dropdown Button Not Switching Selected Item | Flutter
Guide to Fix Dropdown Button Not Switching Selected Item | Flutter
 
Getting Started with Developing for the Apple Watch
Getting Started with Developing for the Apple WatchGetting Started with Developing for the Apple Watch
Getting Started with Developing for the Apple Watch
 
Software engineering modeling lab lectures
Software engineering modeling lab lecturesSoftware engineering modeling lab lectures
Software engineering modeling lab lectures
 
Kbox 101 1000 slide
Kbox 101 1000 slideKbox 101 1000 slide
Kbox 101 1000 slide
 
Information about Toggle Button.pdf
Information about Toggle Button.pdfInformation about Toggle Button.pdf
Information about Toggle Button.pdf
 
Tutorials2
Tutorials2Tutorials2
Tutorials2
 
Twitter trends
Twitter trendsTwitter trends
Twitter trends
 
Gui in matlab :
Gui in matlab :Gui in matlab :
Gui in matlab :
 
67
6767
67
 
A Primavera Tutorial
A Primavera TutorialA Primavera Tutorial
A Primavera Tutorial
 
Android User Interface: Basic Form Widgets
Android User Interface: Basic Form WidgetsAndroid User Interface: Basic Form Widgets
Android User Interface: Basic Form Widgets
 
Ios actions and outlets
Ios actions and outletsIos actions and outlets
Ios actions and outlets
 
Ios actions and outlets
Ios actions and outletsIos actions and outlets
Ios actions and outlets
 
Toolbar, statusbar, coolbar in vb
Toolbar, statusbar, coolbar in vbToolbar, statusbar, coolbar in vb
Toolbar, statusbar, coolbar in vb
 
Gui builder
Gui builderGui builder
Gui builder
 
Android action bar and notifications-chapter16
Android action bar and notifications-chapter16Android action bar and notifications-chapter16
Android action bar and notifications-chapter16
 
Introduction
IntroductionIntroduction
Introduction
 
Using prime[31] to connect your unity game to azure mobile services
Using prime[31] to connect your unity game to azure mobile servicesUsing prime[31] to connect your unity game to azure mobile services
Using prime[31] to connect your unity game to azure mobile services
 
9b4c1 vb(pd) (2)
9b4c1 vb(pd) (2)9b4c1 vb(pd) (2)
9b4c1 vb(pd) (2)
 

More from Ketan Raval

Amazon Alexa Auto Software Development Kit (SDK)
Amazon Alexa Auto Software Development Kit (SDK)Amazon Alexa Auto Software Development Kit (SDK)
Amazon Alexa Auto Software Development Kit (SDK)Ketan Raval
 
Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...
Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...
Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...Ketan Raval
 
Zero ui future is here
Zero ui   future is hereZero ui   future is here
Zero ui future is hereKetan Raval
 
Android n and beyond
Android n and beyondAndroid n and beyond
Android n and beyondKetan Raval
 
IoT and Future of Connected world
IoT and Future of Connected worldIoT and Future of Connected world
IoT and Future of Connected worldKetan Raval
 
#Instagram API Get visibility you always wanted
#Instagram API   Get visibility you always wanted#Instagram API   Get visibility you always wanted
#Instagram API Get visibility you always wantedKetan Raval
 
Keynote - Devfest 2015 organized by GDG Ahmedabad
Keynote - Devfest 2015 organized by GDG AhmedabadKeynote - Devfest 2015 organized by GDG Ahmedabad
Keynote - Devfest 2015 organized by GDG AhmedabadKetan Raval
 
How to make your Mobile App HIPPA Compliant
How to make your Mobile App HIPPA CompliantHow to make your Mobile App HIPPA Compliant
How to make your Mobile App HIPPA CompliantKetan Raval
 
3 d touch a true game changer
3 d touch a true game changer3 d touch a true game changer
3 d touch a true game changerKetan Raval
 
OBD Mobile App - Fault Codes, Driving Behaviour and Fuel Economy
OBD Mobile App - Fault Codes, Driving Behaviour and Fuel EconomyOBD Mobile App - Fault Codes, Driving Behaviour and Fuel Economy
OBD Mobile App - Fault Codes, Driving Behaviour and Fuel EconomyKetan Raval
 
Vehicle to vehicle communication using gps
Vehicle to vehicle communication using gpsVehicle to vehicle communication using gps
Vehicle to vehicle communication using gpsKetan Raval
 
Obd how to guide
Obd how to guideObd how to guide
Obd how to guideKetan Raval
 
Garmin api integration
Garmin api integrationGarmin api integration
Garmin api integrationKetan Raval
 
Beacon The Google Way
Beacon The Google WayBeacon The Google Way
Beacon The Google WayKetan Raval
 
Edge detection iOS application
Edge detection iOS applicationEdge detection iOS application
Edge detection iOS applicationKetan Raval
 
Big data cloudcomputing
Big data cloudcomputingBig data cloudcomputing
Big data cloudcomputingKetan Raval
 
All about Apple Watchkit
All about Apple WatchkitAll about Apple Watchkit
All about Apple WatchkitKetan Raval
 
How to upload application on iTune store
How to upload application on iTune storeHow to upload application on iTune store
How to upload application on iTune storeKetan Raval
 
Beta testing guidelines for developer
Beta testing guidelines for developerBeta testing guidelines for developer
Beta testing guidelines for developerKetan Raval
 

More from Ketan Raval (20)

Amazon Alexa Auto Software Development Kit (SDK)
Amazon Alexa Auto Software Development Kit (SDK)Amazon Alexa Auto Software Development Kit (SDK)
Amazon Alexa Auto Software Development Kit (SDK)
 
Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...
Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...
Proximity Marketing Solutions enhancing Businesses leveraging iBeacon SDK Int...
 
Keynote 2016
Keynote 2016Keynote 2016
Keynote 2016
 
Zero ui future is here
Zero ui   future is hereZero ui   future is here
Zero ui future is here
 
Android n and beyond
Android n and beyondAndroid n and beyond
Android n and beyond
 
IoT and Future of Connected world
IoT and Future of Connected worldIoT and Future of Connected world
IoT and Future of Connected world
 
#Instagram API Get visibility you always wanted
#Instagram API   Get visibility you always wanted#Instagram API   Get visibility you always wanted
#Instagram API Get visibility you always wanted
 
Keynote - Devfest 2015 organized by GDG Ahmedabad
Keynote - Devfest 2015 organized by GDG AhmedabadKeynote - Devfest 2015 organized by GDG Ahmedabad
Keynote - Devfest 2015 organized by GDG Ahmedabad
 
How to make your Mobile App HIPPA Compliant
How to make your Mobile App HIPPA CompliantHow to make your Mobile App HIPPA Compliant
How to make your Mobile App HIPPA Compliant
 
3 d touch a true game changer
3 d touch a true game changer3 d touch a true game changer
3 d touch a true game changer
 
OBD Mobile App - Fault Codes, Driving Behaviour and Fuel Economy
OBD Mobile App - Fault Codes, Driving Behaviour and Fuel EconomyOBD Mobile App - Fault Codes, Driving Behaviour and Fuel Economy
OBD Mobile App - Fault Codes, Driving Behaviour and Fuel Economy
 
Vehicle to vehicle communication using gps
Vehicle to vehicle communication using gpsVehicle to vehicle communication using gps
Vehicle to vehicle communication using gps
 
Obd how to guide
Obd how to guideObd how to guide
Obd how to guide
 
Garmin api integration
Garmin api integrationGarmin api integration
Garmin api integration
 
Beacon The Google Way
Beacon The Google WayBeacon The Google Way
Beacon The Google Way
 
Edge detection iOS application
Edge detection iOS applicationEdge detection iOS application
Edge detection iOS application
 
Big data cloudcomputing
Big data cloudcomputingBig data cloudcomputing
Big data cloudcomputing
 
All about Apple Watchkit
All about Apple WatchkitAll about Apple Watchkit
All about Apple Watchkit
 
How to upload application on iTune store
How to upload application on iTune storeHow to upload application on iTune store
How to upload application on iTune store
 
Beta testing guidelines for developer
Beta testing guidelines for developerBeta testing guidelines for developer
Beta testing guidelines for developer
 

Recently uploaded

Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Pooja Nehwal
 
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost LoverPowerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost LoverPsychicRuben LoveSpells
 
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCRFULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCRnishacall1
 
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort ServiceBDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort ServiceDelhi Call girls
 
9892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x79892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x7Pooja Nehwal
 
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceCALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceanilsa9823
 
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceCALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceanilsa9823
 

Recently uploaded (7)

Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
 
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost LoverPowerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
Powerful Love Spells in Arkansas, AR (310) 882-6330 Bring Back Lost Lover
 
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCRFULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
FULL ENJOY - 9999218229 Call Girls in {Mahipalpur}| Delhi NCR
 
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort ServiceBDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
BDSM⚡Call Girls in Sector 71 Noida Escorts >༒8448380779 Escort Service
 
9892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x79892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x7
 
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceCALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
 
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceCALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
 

Google calendar integration in iOS app