Syncing two calendars (e.g. Exchange into Google Calendar) on iOS and Android

Sometimes people are allowed to see their work calendar on their Android phone or iPhone. But they cannot share the calendar directly with their family to show them when they have work events. For this purpose it would be useful to mirror the work calendar (e.g. the Exchange calendar) into a Google calendar in their … Continue reading “Syncing two calendars (e.g. Exchange into Google Calendar) on iOS and Android”

Sometimes people are allowed to see their work calendar on their Android phone or iPhone. But they cannot share the calendar directly with their family to show them when they have work events. For this purpose it would be useful to mirror the work calendar (e.g. the Exchange calendar) into a Google calendar in their Google account so that they can share it with their family. By mirroring only the times of the events but not the contents (title, location, description) confidential data can be removed and only the important data (the time of the events) is shared.

If you have that problem and would like your family to see your work Exchange calendar, then just do the following:

  1. Create a new empty calendar e.g. “Work” in your Google Calendar account.
  2. Sync your Android phone/iPhone with your Exchange account and your Google Calendar account so that you can see calendars from both accounts.
  3. Install SyncCal for Android ( https://play.google.com/store/apps/details?id=com.calengoo.synccal ) or SyncCal for iOS ( https://itunes.apple.com/us/app/synccal/id796482010?mt=8 ) on your phone and use it to copy the Exchange calendar into your new “Work” calendar in your Google account.
  4. Check if you can see your work events in Google Calendar. If that works fine, you can share the calendar with your family to let them see your work events.

Apple AppStore Available on Google Play

Google’s new OAuth2 mechanism for native apps

So far the OAuth2 authentication could be performed with embedded browsers in iOS and Android. However Google has decided that they want apps to use the default browser instead now. One reason is that the default browser might already know the user due to cookies and can shorten the authentication process. The other reason might … Continue reading “Google’s new OAuth2 mechanism for native apps”

So far the OAuth2 authentication could be performed with embedded browsers in iOS and Android. However Google has decided that they want apps to use the default browser instead now. One reason is that the default browser might already know the user due to cookies and can shorten the authentication process. The other reason might be that apps could have theoretically captured the entered password when using an embedded browser. Anyway to switch an iOS app from the old authentication method using GTMOAuth2ViewControllerTouch to the new using OIDAuthorizationRequest requires only a few steps due to the useful library GTMAppAuth:

  1. Create a file called “Podfile” and use it to download and install GTMAppAuth:
    target 'YourProjectName' do
     platform :ios, '7.0'
     pod 'GTMAppAuth'
    end
  2. Install Cocoapods if necessary and then run
    pod install
  3. Open the created Xcode workspace file. Delete the following old files if you have them:
    GTMGatherInputStream.h
    GTMGatherInputStream.m
    GTMMIMEDocument.h
    GTMMIMEDocument.m
    GTMReadMonitorInputStream.h
    GTMReadMonitorInputStream.m
  4. Add a property to your AppDelegate.h file:
    @property(nonatomic, retain) id currentAuthorizationFlow;
  5. Add a handler to your AppDelegate.m file in the application:openURL:sourceApplication:annotation: method. Put this right at the start of the method:
    if ([_currentAuthorizationFlow resumeAuthorizationFlowWithURL:url]) {
     _currentAuthorizationFlow = nil;
     return YES;
    }
  6. Now you are ready to replace your old authentication code which used GTMOAuth2ViewControllerTouch with the new code (taken partially from the GTMAppAuth website):
    OIDServiceConfiguration *configuration = [GTMAppAuthFetcherAuthorization configurationForGoogle];
    
    OIDAuthorizationRequest *request = [[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
     clientId:GOOGLE_CLIENT_ID
     clientSecret:GOOGLE_CLIENT_SECRET
     scopes:@[OIDScopeEmail, @"https://www.googleapis.com/auth/calendar"]
     redirectURL:[NSURL URLWithString:@"com.example.yourapp:/oauthredirect"]
     responseType:OIDResponseTypeCode
     additionalParameters:nil] autorelease];
    
     // performs authentication request
     AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
     appDelegate.currentAuthorizationFlow =
     [OIDAuthState authStateByPresentingAuthorizationRequest:request
     presentingViewController:self
     callback:^(OIDAuthState *_Nullable authState,
     NSError *_Nullable error) {
     if (authState) {
     // Creates the GTMAppAuthFetcherAuthorization from the OIDAuthState.
     GTMAppAuthFetcherAuthorization *authorization = [[[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState] autorelease];
  7. Using the authorization result you can now call your existing handler:
    [self viewControllerFinishedWithAuth:authorization error:error accessToken:authState.lastTokenResponse.accessToken];
     NSLog(@"Got authorization tokens. Access token: %@",
     authState.lastTokenResponse.accessToken);
     } else {
     NSLog(@"Authorization error: %@", [error localizedDescription]);
     }
     }];
    
    }
    
    - (void)viewControllerFinishedWithAuth:(id )auth error:(NSError *)error accessToken:(NSString *)accessToken {
     if (error == nil) {
     // OAuth2 Login was successful
     // Serialize to Keychain
     [GTMOAuth2KeychainCompatibility saveAuthToKeychainForName:keychainNameOAuth2
     authentication:auth];

    Now you can use “accessToken” to access Google services. And due to GTMOAuth2KeychainCompatibility you can keep your existing functions that load the authentication data from the keychain.

  8. In the Info.plist file of your app you have to configure the “com.example.yourapp” URL mentioned above so that the browser can open your app and send you the result:
    CFBundleURLTypes
    
        
            CFBundleTypeRole
            Editor
            CFBundleURLSchemes
            
                com.example.yourapp
            
        
    
    

So that’s all you have to change for an existing iOS app. You can find further information in Google’s blogpost.

UI Tests with XCode 7 and XCTestCase

In XCode 7 direct support for two types of tests has been added: Unit tests: These tests can access your project’s classes and are directly linked to your project. They are used for low-level tests where you test the functions of your classes. Usually one test class for one project class is created. UI tests: … Continue reading “UI Tests with XCode 7 and XCTestCase”

In XCode 7 direct support for two types of tests has been added:

  • Unit tests: These tests can access your project’s classes and are directly linked to your project. They are used for low-level tests where you test the functions of your classes. Usually one test class for one project class is created.
  • UI tests: These tests run independently of your project and perform UI actions. I.e. they cannot access the classes or data of your app. But they can use the app like a user, i.e. they can interact with it, tap buttons, enter text etc.

To add such tests to your existing project just choose “New > Target” in XCode 7:

Screen Shot 2015-11-01 at 04.05.36

It will create example classes that make it easier to start. To create a new UI test is also simple. Just create an empty function, e.g.

- (void)testEverything {
}

Then place the cursor in this function an click on the record button to start the simulator and record your actions as commands into the method:

Screen Shot 2015-11-01 at 04.20.56

To run the tests when you are finished just long click on the play button in XCode and select “Test”.

Snippets for UI test

Turning a switch on:

if ([tablesQuery.switches[@"My switch label"].value isEqualToString:@"0"]) {
    [tablesQuery.switches[@"My switch label"] tap];
}

Checking the position of a row in a table:

XCTAssertLessThanOrEqual([[app.staticTexts[@"My row label"] coordinateWithNormalizedOffset:CGVectorMake(0, 0)] screenPoint].y, 90);

Searching for a table row that starts with “Until:” (you can find a reference of the BEGINSWITH and similar commands here):

[[app.staticTexts elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label BEGINSWITH 'Until:'"]] tap];

Displaying all currently visible elements:

NSLog(app.debugDescription);

Overview of the available functions:

http://masilotti.com/xctest-documentation/index.html

New iOS 9 attribute readableContentGuide

With iOS 9 the default UITableViewCell looks different on iPads now. It has a larger margin on the left and right side: This margin is used if the attribute cellLayoutMarginsFollowReadableWidth is set, which is turned on by default. That means by default your table view cells will now have this larger margin. However when creating UITableViewCells from … Continue reading “New iOS 9 attribute readableContentGuide”

With iOS 9 the default UITableViewCell looks different on iPads now. It has a larger margin on the left and right side:

Simulator Screen Shot 19.10.2015 11.50.27

This margin is used if the attribute cellLayoutMarginsFollowReadableWidth is set, which is turned on by default. That means by default your table view cells will now have this larger margin. However when creating UITableViewCells from XIB files, you have to use Auto-Layout and you have to turn “Preserve Superview Margin” and “Follow Readable Width” on for the “Content View” of your UITableViewCell to make your custom cell use the same margins:

AccountTableViewCell_xib

And when creating horizontal Auto-Layout constraints you have to ensure that “Constrain to margins” is checked:

Screen_Shot_2015-10-19_at_12_07_06

Otherwise your custom cells would have no margin and would not be correctly aligned with the other rows in the table.

iOS 11 and iPhone X

The same is now also necessary to display tables in landscape mode on the iPhone X. Because the iPhone X has a notch that is at the side of the screen in landscape mode and can overlap the text of the table cells if the readableContentGuide is not used.

E.g. to programmatically align “textLabel” correctly you could write this:

[self addConstraint:[textLabel.leadingAnchor constraintEqualToAnchor:self.readableContentGuide.leadingAnchor]];
[self addConstraint:[textLabel.trailingAnchor constraintEqualToAnchor:self.readableContentGuide.trailingAnchor]];

If you have tried the above and it still doesn’t work then click on the leading/trailing constraints one by one and ensure that the “Relative to margin” checkmark is set:

iPhone X and custom table headers

When using

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section

to set a custom header for your table view you have to return a normal view. If you use a XIB file you can just check “Follow Readable Width”. But if you are creating the view programmatically, you have to return a view that contains another view (like the contentView of a table cell) and that view then contains your content, e.g. “view(view(label))”. E.g. like this if “self” is the topmost view:

UIView *pane = [[[UIView alloc] init] autorelease];
pane.translatesAutoresizingMaskIntoConstraints = FALSE;
[self addSubview:pane];

[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[pane]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(pane)]];
UILayoutGuide *guide = self.readableContentGuide;
[guide.leadingAnchor constraintEqualToAnchor:pane.leadingAnchor].active = TRUE;
[guide.trailingAnchor constraintEqualToAnchor:pane.trailingAnchor].active = TRUE;

[pane addSubview:textLabel];

self.textLabel = [[[UILabel alloc] init] autorelease];
textLabel.translatesAutoresizingMaskIntoConstraints = FALSE;
[pane addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[textLabel]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(textLabel)]];
[pane addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[textLabel]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(textLabel)]];