How to authenticate your iOS app using the Spotify iOS SDK and the SFSafariViewController

Leave a comment

To help me understand all the moving pieces of the Spotify iOS SDK I created this picture here showing how to setup authentication for a basic ViewController app.

Screen Shot 2017-04-19 at 2.27.18 PM.png

Spotify App

First you need to create a Spotify application and set your clientID, redirect URI, the application bundle id of your app (else SSO won’t work).

Screen Shot 2017-04-19 at 2.40.26 PM.png

AppDelegate

There are two parts here. One sets up the SPTAuth object with all your client id details (that it reads from a local Config.h file you create).

And the other notifies the ViewController once your app has been authenticated.

AppDelegate.h

Screen Shot 2017-04-19 at 2.42.30 PM.png

#import "AppDelegate.h"

#import <SpotifyAuthentication/SpotifyAuthentication.h>
#import <SpotifyMetadata/SpotifyMetadata.h>
#import <SpotifyAudioPlayback/SpotifyAudioPlayback.h>
#import "Config.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Set up shared authentication information
    SPTAuth *auth = [SPTAuth defaultInstance];
    auth.clientID = @kClientId;
    auth.requestedScopes = @[SPTAuthStreamingScope];
    auth.redirectURL = [NSURL URLWithString:@kCallbackURL];
    auth.sessionUserDefaultsKey = @kSessionUserDefaultsKey;
    return YES;
}

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    SPTAuth *auth = [SPTAuth defaultInstance];

    SPTAuthCallback authCallback = ^(NSError *error, SPTSession *session) {
        // This is the callback that'll be triggered when auth is completed (or fails).

        if (error) {
            NSLog(@"*** Auth error: %@", error);
        } else {
            auth.session = session;
        }
        [[NSNotificationCenter defaultCenter] postNotificationName:@"sessionUpdated" object:self];
    };

    /*
     Handle the callback from the authentication service. -[SPAuth -canHandleURL:]
     helps us filter out URLs that aren't authentication URLs (i.e., URLs you use elsewhere in your application).
     */

    if ([auth canHandleURL:url]) {
        [auth handleAuthCallbackWithTriggeredAuthURL:url callback:authCallback];
        return YES;
    }

    return NO;
}

@end

Config.h

Screen Shot 2017-04-19 at 2.45.13 PM.png

#ifndef Config_h
#define Config_h

// Your client ID
#define kClientId "xxx"

// Your applications callback URL
#define kCallbackURL "messages-app://"

#define kSessionUserDefaultsKey "SpotifySession"

#endif /* Config_h */

Info.plist

In order for the Spotify SafariController to flip back to your app, your app needs to support a URL Schema – the same one your defined in your Spotify application.

Screen Shot 2017-04-19 at 2.48.40 PM.png

If you don’t set this you will see an error like this

Screen Shot 2017-04-19 at 2.03.29 PM.png

ViewController

Here we listen for notifications from our AppDelegate when we get the OK that we are authenticated.

Screen Shot 2017-04-19 at 2.52.27 PM.png

And secondly we open a SafariViewController with a URL we get from our SPTAuth default instance.

Screen Shot 2017-04-19 at 2.53.44 PM.png

This line here is where all the magic happens. It works because we setup the values for it in AppDelegate. If you don’t do that there you will get an error like this – “Missing clientId”

Screen Shot 2017-04-19 at 2.04.14 PM.png

When you print out this URL it looks like this:

https://accounts.spotify.com/authorize?
nolinks=true&nosignup=true&
response_type=token&
scope=streaming&
utm_source=spotify-sdk&
utm_medium=ios-sdk&
utm_campaign=ios-sdk&
redirect_uri=messages-app%3A%2F%2F&
show_dialog=true&
client_id=xxx

This is the URL we open with our Safari controller which leads to a flow of screens like this.

Screen Shot 2017-04-19 at 2.35.09 PM.png

Screen Shot 2017-04-19 at 2.35.14 PM.png

Screen Shot 2017-04-19 at 3.00.18 PM.png

And once you login, it should all work and you will see a log message like so.

Screen Shot 2017-04-19 at 3.02.53 PM.png

iMessage crash with keyboard compact view

Leave a comment

So iMessage doesn’t like it when you try to open a keyboard in compact view.

It also doesn’t like it if you swipe to another iMessage app by touching a search bar or text field, as that activates the keyboard and kills the composer.

The way to fix this is to not allow the keyboard to show while in compact view. I did this by setting a flag in the ViewController that houses the searchbar, and then only responding YES to shouldBegin editing if this was set.

MessageViewController.m

- (void)willTransitionToPresentationStyle:(MSMessagesAppPresentationStyle)presentationStyle
{
    if (presentationStyle == MSMessagesAppPresentationStyleCompact) {
        self.searchViewController.isPresentationStyleCompact = YES;
        [self presentViewController:self.searchViewController];
    } else {
        self.searchViewController.isPresentationStyleCompact = NO;
    }
}

SearchViewController.m

- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar
{
    if (self.isPresentationStyleCompact) {
        return NO;
    } else {
        return YES;
    }
}

Links

http://stackoverflow.com/questions/39361172/imessage-app-crash-in-expanded-mode-with-keyboard-of-textfield

https://forums.developer.apple.com/thread/52850

OCMockito Examples

Leave a comment

I got tired of not understanding how OCMockito worked and created the following dirt simple examples to show me how.

SimpleTest.m

#import <XCTest/XCTest.h>

#import "Handler.h"
#import "Transport.h"

#import <OCHamcrestIOS/OCHamcrestIOS.h>
#import <OCMockitoIOS/OCMockitoIOS.h>


@interface HandlerTest : XCTestCase
@property(nonatomic, strong) Handler *sut;
@property(nonatomic, strong) Transport *mockTransport;
@end

@implementation HandlerTest

- (void)setUp
{
    [super setUp];
    self.mockTransport = mock([Transport class]);
    self.sut = [[Handler alloc] initWithTransport:self.mockTransport];
}

- (void)testSimpleVerify
{
    [self.sut simpleMethod];
    [verify(self.mockTransport) foo];
}

- (void)testSimpleVerifyWithSpecificArgument
{
    [self.sut simpleMethodWithArgument:@"bar"];
    [verify(self.mockTransport) fooWithArgument:@"bar"];
}

- (void)testSimpleStub
{
    [given([self.mockTransport connected]) willReturnBool:YES];
    XCTAssert([self.sut isTransportConnected]);
}

- (void)testMethodWithCallback
{
    [self.sut simpleMethodWithCallback];
    [verify(self.mockTransport) fooWithCallback:anything()];
}

- (void)testMethodWithCallbackAndParameters
{
    [self.sut simpleMethodWithCallbackAndParameters];

    HCArgumentCaptor *argument = [HCArgumentCaptor new];
    [verify(self.mockTransport) fooWithCallbackAndParameters:(id)argument];

    void (^callback)(NSArray*, BOOL success) = argument.value;

    NSArray* result = [NSArray new];
    callback(result, YES);

    assertThat(self.sut.model, equalTo(result));
}

@end

Handler.h

#import <Foundation/Foundation.h>
#import "Transport.h"

@interface Handler : NSObject

- (instancetype)initWithTransport:(Transport *)transport;

@property (nonatomic, strong) NSArray *model;

- (void)simpleMethod;
- (void)simpleMethodWithArgument:(NSString *)argument1;
- (void)simpleMethodWithCallback;
- (void)simpleMethodWithCallbackAndParameters;
- (BOOL)isTransportConnected;

@end

Handler.m

#import "Handler.h"
#import "Transport.h"

@interface Handler()
@property (nonatomic, strong) Transport *transport;
@end

@implementation Handler

- (instancetype)initWithTransport:(Transport *)transport
{
    self = [super init];
    if (self) {
        _transport = transport;
    }
    return self;
}

- (void)simpleMethod
{
    [self.transport foo];
}

- (void)simpleMethodWithArgument:(NSString *)argument1
{
    [self.transport fooWithArgument:argument1];
}

- (BOOL)isTransportConnected
{
    return self.transport.connected;
}

- (void)simpleMethodWithCallback
{
    [self.transport fooWithCallback:^(id data, NSError *error) {

    }];
}

- (void)simpleMethodWithCallbackAndParameters
{
    [self.transport fooWithCallbackAndParameters:^(NSArray *result, BOOL success) {
        if (success) {
            self.model = result;
        }
    }];
}

@end

Transport.h

#import <Foundation/Foundation.h>

@interface Transport : NSObject
@property (nonatomic, assign) BOOL connected;
- (void)foo;
- (void)fooWithArgument:(NSString *)argument1;
- (void)fooWithCallback:(void(^)(id data, NSError *error))block;
- (void)fooWithCallbackAndParameters:(void(^)(NSArray *result, BOOL success))block;
@end

Transport.m

#import "Transport.h"

@implementation Transport
- (void)foo {}
- (void)fooWithArgument:(NSString *)argument1 {}
- (void)fooWithCallback:(void(^)(id data, NSError *error))block {}
- (void)fooWithCallbackAndParameters:(void(^)(NSArray *result, BOOL success))block {}
@end

Unit testing tip – additional override

Leave a comment

If you have have a method you want to test, but it does some stuff you like for convenience, and you don’t really want to mess for the sake of testing, create with some overrides and do your testing there.

+ (void)foo:(NSURLSessionConfiguration *)sessionConfiguration
{
    TokenManager *tokenManager = [[TokenManager alloc] init];
    AccessToken *accessToken = tokenManager.accessToken;

    [self addAuthorizationHeaderToSessionConfiguruation:sessionConfiguration token:accessToken];
}

+ (void)foo:(NSURLSessionConfiguration *)sessionConfiguration token:(AccessToken *)accessToken
{
    NSString *accessTokenHeaderValue = [NSString stringWithFormat:@"%@ %@", accessToken.tokenType, accessToken.token];
    sessionConfiguration.HTTPAdditionalHeaders = @{@"Authorization" : accessTokenHeaderValue};
}

The first method is the convenient public one.
The second is the one you can test and inject other things into.

How to encode something on a background thread and then post it on the main one iOS

Leave a comment

// encode image on background thread
                           dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
                               NSData *imageData = result.namedArguments[@"image_data"];
                               UIImage *image = [UIImage imageWithData:imageData];

                               // callback on main thread
                               dispatch_async(dispatch_get_main_queue(), ^{
                                   callback(image, nil);
                               });

How to redraw a UITable or UICollection with change of device orientation iOS

Leave a comment

Tables and collections need to be redrawn when you change from portrait to landscape. Listen for the event, then do the redraw like this.

#pragma mark - Orientation

- (void)addOrientationObserver
{
    [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(orientationChanged:)
                                                 name:UIApplicationDidChangeStatusBarOrientationNotification
                                               object:nil];

}

- (void)orientationChanged:(NSNotification *)notification
{
    [self.tableView reloadData];
}

How to determine controls sizes iOS applications

Leave a comment

Knowing what the right size of a component in in iOS can be tough sometimes. Yes you can query it at run time, and yes you can use tools like reveal, but sometimes among all the various displays and screens you just want to know how big this status bar is.

One way to do that is like this.

Run the simulator. Take a screen shot of your app and control drag the distance in preview.

Screen Shot 2017-04-06 at 7.57.36 AM.png

 

This navigation bar, in portrait mode, says 128. Which means it will be 64 in my app after taking retina into account.

Note: This method isn’t fool proof. And I still recommend googling for the dimensions you are looking for.

Older Entries

%d bloggers like this: