Apple Music for iOS developers (hint: it doesn’t work)

Back in June Apple released its own music streaming service, Apple Music, and to this date there is no API that allows developers to easily take advantage of the user’s Apple Music library. Sure, MPMediaPickerController and MPMusicPlayerController work great with very few lines of code, but the player doesn’t work in the background and that’s a very big limitation.
AVFoundation’s AVPlayer and AVAudioPlayer are two very powerful options when it comes to audio and video playback and they work in the background, but they both have a problem: they can’t play directly the MPMediaItems that the MPMediaPickerController returns.
Before Apple Music (and iTunes Match), every MPMediaItem used to represent a single song that was physically stored on the user’s device, so the quickest option was to access its MPMediaItemPropertyAssetURL and use it for playback. Today, there is a range of different situations and the AssetURL isn’t always available.

Song origin/location AssetURL
iTunes (Win/Mac OSX transfer) YES
iTunes (downloaded via iOS app) YES
iTunes Match (stored on device) YES
iTunes Match (stored on iCloud) NO
Apple Music (stored on device) NO
Apple Music (stored on iCloud) NO

As expected, an iTunes Match or Apple Music song that is only in the cloud doesn’t have a local AssetURL, but Apple Music songs aren’t available even if the user decided to save them for offline use. Since there is no support for Apple Music, if we want to let the user pick a song to be played with AVAudioPlayer, we need to be able to hide the ones that don’t have a valid AssetURL. By default MPMediaPickerController shows every song in the library (including cloud items), so the only option is to create our own song picker using MPMediaQuery. This class doesn’t allow to filter directly the songs based on their AssetURL, but once we have the complete list we can cycle through the array and remove the ones who don’t have a valid URL:

// Get all the songs from the library
MPMediaQuery *query = [MPMediaQuery songsQuery];

// Filter out cloud items
[query addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithBool:NO] forProperty:MPMediaItemPropertyIsCloudItem]];

NSArray *items = [query items];
NSMutableArray *itemsWithAssetURL = [NSMutableArray array];

// For every song stored locally
for (MPMediaItem *item in items)
{
    // If the song has an assetURL
    if ([item valueForProperty:MPMediaItemPropertyAssetURL]) {
        // Add it to the array of valid songs
        [itemsWithAssetURL addObject:item];
    }
}

This does the trick, but if the user’s library contains thousands of songs (as it does most of the times) it’s probably best to perform this away from the main thread and/or to limit the number of items to check in the first place.

Creating a two-dimensional scroll interface for iOS using UIPageView and UICollectionView

Recently I’ve been looking into ways to create an interface that allows the user to navigate through full-page views using both vertical and horizontal scrolling. Each view provides a data chart (replaced by a simple label in this example) for a specific day, week or month. By swiping left/right the user can navigate between days, while swiping up/down they can switch to the weekly (and further down, monthly) view and back up. The navigation works in a very similar way to Sony PS3’s and PS4’s menu interface.

iOS provides different ways to display scrolling and paged content, namely UIPageView, UIScrollView and UICollectionView. The last of the three was the last to be introduced and it is the most versatile thanks to the ability to use a custom UICollectionViewLayout to create a grid interface.
At first, a grid interface with only one view at a time on-screen seemed like the way to go, but then the horizontal and vertical navigation would have been tied together: if the user navigates 3 days back in time (horizontally) and then swipes down to the week view, they would be provided with the data from 3 weeks back, and this would be counterintuitive and confusing.

The solution was to have three different UICollectionView to handle the horizontal navigation independently and a UIPageView to navigate vertically between the three.

I added a UIPageViewController to the storyboard, and in the Attribute Inspector I set the navigation direction to vertical and the transition style to scroll. This will be the initial view controller.

Then I added a UICollectionViewController and set its storyboard ID to “collectionViewController” in the Identity Inspector. I navigated the view hierarchy down one level to the UICollectionView and configured it as follows.

The UIPageView is handled by VerticalPageViewController:

@implementation VerticalPageViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Set yourself as the datasource and delegate of the page view
    self.dataSource = self;
    self.delegate = self;

    // Get the first of the three UICollectionViewControllers and load it on screen
    HorizontalCollectionViewController *startingViewController = [self viewControllerAtIndex:0];
    NSArray *viewControllers = @[startingViewController];
    [self setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
}

#pragma mark - UIPageViewController Delegate & Datasource

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    NSInteger index = ((HorizontalCollectionViewController *) viewController).pageIndex;

    if (index == NSNotFound) {
        return nil;
    }

    index--;

    return [self viewControllerAtIndex:index];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    NSInteger index = ((HorizontalCollectionViewController *) viewController).pageIndex;

    if (index == NSNotFound) {
        return nil;
    }

    index++;

    return [self viewControllerAtIndex:index];
}

- (HorizontalCollectionViewController *)viewControllerAtIndex:(NSInteger)index
{
    if ((index > 0) || (index < -2)) {
        return nil;
    } else {

        // Get a new UICollectionViewController (with all the settings and subviews) from the storyboard
        HorizontalCollectionViewController *collectionViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"collectionViewController"];
        // Give the controller a page index so it is aware of which data to display (day/week/month)
        collectionViewController.pageIndex = index;

        return collectionViewController;
    }
}       

@end

The three UICollectionView are handled by a single controller, HorizontalCollectionViewController.m.

@interface HorizontalCollectionViewController ()

@property IBOutlet UILabel *label;

@property NSMutableArray *dailyContent;
@property NSMutableArray *weeklyContent;
@property NSMutableArray *monthlyContent;

@end

@implementation HorizontalCollectionViewController

static NSString * const reuseIdentifier = @"Cell";

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Register cell classes
    // [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];

    // Do any additional setup after loading the view.
    _dailyContent = [[NSMutableArray alloc] initWithArray:@[@"DAY: One", @"DAY: Two", @"DAY: Three", @"DAY: Four", @"DAY: Five"]];
    _weeklyContent = [[NSMutableArray alloc] initWithArray:@[@"WEEK: One", @"WEEK: Two", @"WEEK: Three", @"WEEK: Four", @"WEEK: Five"]];
    _monthlyContent = [[NSMutableArray alloc] initWithArray:@[@"MONTH: One", @"MONTH: Two", @"MONTH: Three", @"MONTH: Four", @"MONTH: Five"]];

    [self scrollToEnd];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

#pragma mark <UICollectionViewDataSource>

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return 1;
}


- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    // Depending on the page index (0 to -2) figure out which data to display
    switch (_pageIndex)
    {
        case 0: {
            return [_dailyContent count];
            break;
        }

        case -1: {
            return [_weeklyContent count];
            break;
        }


        case -2: {
            return [_monthlyContent count];
            break;
        }

        default: {
            return 1;
            break;
        }
    }
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // Don't forget to give the UICollectionViewCell a class and a storyboard ID in the Interface Builder
    // HorizontalCollectionViewCell.h declared a IBOutlet UILabel *label
    // This way the compiler knows that each cell has a label and doesn't complain (wire the outlet to the placeholder cell in the Interface Builder)
    HorizontalCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MovesCell" forIndexPath:indexPath];

    // Get the content from the correct array and update the label
    switch (_pageIndex)
    {
        case 0: {
            cell.label.text = [_dailyContent objectAtIndex:indexPath.row];
            break;
        }

        case -1: {
            cell.label.text = [_weeklyContent objectAtIndex:indexPath.row];
            break;
        }


        case -2: {
            cell.label.text = [_monthlyContent objectAtIndex:indexPath.row];
            break;
        }

        default: {
            break;
        }
    }

    return cell;
}

#pragma mark UICollectionViewDelegate

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // Each cell must be the same as the screen size minus the height of the status bar (otherwise it will brake the UICollectionViewFlowLayout)
    CGFloat width = self.view.frame.size.width;
    CGFloat height = self.view.frame.size.height - [UIApplication sharedApplication].statusBarFrame.size.height;

    return CGSizeMake(width, height);
}

@end

The header files for these two classes just declare the public properties and the protocols the two classes conform to. Using a custom subclass of UICollectionViewCell allows to declare its outlets and to design its interface easily in the storyboard.

If everything has been wired and set correctly in the storyboard, these few lines of code will allow you to create a pretty neat and complex UI!

Adopting Multitasking Enhancements on iPad: Getting Oriented

Adopting Multitasking Enhancements on iPad: Getting Oriented

iOS/OS X network testing with XCTest

I recently worked on a OS X app which had to keep in sync with a database running on a remote server, to interact with a hardware device via serial port and to allow the user to edit said database with information coming from the hardware device. Since reinventing the wheel is never a good idea, I decided to use various frameworks for those tasks: ORSSerialPort to handle the serial communication, AFNetworking for the networking, Apple’s own Core Data for the local database and XCTest together with OHHTTPStubs for the testing. The networking part was by far the easiest to develop but also the hardest to test, due to its asynchronicity.

This is basically how XCTest works: you create subclasses of XCTestCase, and then define instance methods with their name starting with “test”. Inside these methods you can check for whatever condition you need to test. Here’s a very simple example:

- (void)testReverseString{ 

    NSString *originalString = @"test";
    NSString *reversedString = [self.controllerToTest reverseString:originalString];
    NSString *expectedReversedString = @"tset";

    XCTAssertEqualObjects(expectedReversedString, reversedString);
}

XCTAssert allows to easily compare variables and to let tests fail/succeed based on the result, but it is not enough when it comes to testing the networking code. For example, I needed to test a GET request that fetched the user database from the remote server and tried to do so this way:

- (void)testSuccessfulUserFetch
{

    // Perform fetch
    [self.networkManager fetchUsers];

    // Test if the number of users created in the local database is equal to the one in the remote database
    NSArray *localUserDatabase = [User usersInManagedObjectContext:self.moc];
    XCTAssertEqual([localUserDatabase count], 10);

}

The test failed even if the GET was performed with success, because the local database was tested right after the network request was started and not after its completion.

Luckily Xcode 6 introduced a new feature in XCTest that allows to test asynchronous code as well: XCTestExpectation. It’s as easy as declaring an expectation and setting a timeout for that expectation. The only other thing that’s needed is of course to check for whatever condition has to be met and to fulfill the expectation if that happens.

The obvious place to check and fulfill my network expectations was inside the success/failure blocks that my network manager used, but since that code was written way before I decided to write tests for it, there was no easy way to do so without refactoring. At first I felt like it was not a very good idea to refactor the network manager just to be able to test it, but I’m glad I did: while the existing code was working fine, refactoring it and being able to test it afterwards helped a lot while further developing it.

This is how I changed on of my NetworkManger.m methods to use optional success/failure blocks:

typedef void(^SuccessBlock)(AFHTTPRequestOperation *operation, id responseObject);
typedef void(^FailureBlock)(AFHTTPRequestOperation *operation, NSError *error);

// Perform network request for user database
- (void)fetchUsersWithSuccess:(SuccessBlock)optionalSuccessBlock
         withFailure:(FailureBlock)optionalFailureBlock
{
    // Create success block
    SuccessBlock successBlock = ^(AFHTTPRequestOperation *operation, id responseObject){   

        // Load users into the database
        NSArray *usersArray = (NSArray *)responseObject;
        [User loadUsersFromArray:usersArray inManagedObjectContext:self.managedObjectContext];

        // Optional success block, if given
        if (optionalSuccessBlock) optionalSuccessBlock(operation, responseObject);
    };  

    // Create failure block
    FailureBlock failureBlock = ^(AFHTTPRequestOperation *operation, NSError *error){

        // Optional failure block, if given
        if (optionalFailureBlock) optionalFailureBlock(operation, error);
    };  

    // Perform request to server
    [self.manager GET:self.userSyncURL parameters:nil success:successBlock failure:failureBlock];
}

And here’s what the test method looks like:

- (void)testSuccessfulUserFetch{
    // Create expectation
    XCTestExpectation *countExpectation = [self expectationWithDescription:@"User count expectation"];

    // Create custom success block
    SuccessBlock successBlock = ^(AFHTTPRequestOperation *operation, id responseObject){
        // Check and fulfill expectation
        [self checkUsersAndFullfillCountExpectation:countExpectation];
    };

  // Wait for expectations to be fulfilled. If timeout is met, test fails.
  [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
    if (error) {
      NSLog(@"Timeout Error: %@", error);
    }
  }];
}

// Check expectations for successful user fetch
- (void)checkUsersAndFullfillExpectation:(XCTestExpectation *)countExpectation{

  // Check the users count in the database
  NSArray *users = [User usersInManagedObjectContext:self.moc];
  if ([users count] == 10){
    [countExpectation fulfill];
  }
}