Developing private static library for iOS with CocoaPods

author: Kamil Burczyk

Today I will show you how to create a private static library for iOS which is a CocoaPods project itself and use it in your CocoaPods managed application without pushing it into global Pods repository or even your own public repo. We will also add some unit tests so it can be improved and developed independently.

Create static library

  • First of all let's create a static library project in Xcode and name it however you want (NetworkLib is my call). When you create new project just select Framework & Library/Cocoa Touch Static Library, name it and we can take off.
    Cocoa Touch Static Library creation screen

  • Navigate to your library folder in terminal and type pod init to create empty Podfile.

  • Open Podfile with any text editor and add required dependencies. In my example I will just use AFNetworking. My Podfile looks like this:

platform :ios, "7.0"  
target "NetworkLib" do  
pod 'AFNetworking'  
end  
target "NetworkLibTests" do  
end  
  • Run pod install and command succeeds your project directory should look similar to mine:

Project directory after pod install

  • In order to use CocoaPods project as dependency in application it has to have a podspec file, so let's create one. Type pod spec create NetworkLib to generate it. If you are not familiar with syntax of podspec file take a look at Cocoapods: Creating a Pod Spec.

Update: as Jeff pointed in comments if you wonder where this .podspec file goes it should be created on the same level as Podfile so that its path is NetworkLib/NetworkLib.podspec.

  • Edit NetworkLib.podspec. Mine looks like this:
Pod::Spec.new do |s|  
  s.name         = "NetworkLib"
  s.version      = "0.0.1"
  s.summary      = "A short description of NetworkLib."

  s.description  = <<-DESC
                   A longer description of NetworkLib in Markdown format.
                   DESC

  s.homepage     = "http://EXAMPLE/NetworkLib"
  s.license      = 'MIT (example)'
  s.author       = { "Kamil Burczyk" => "kamil.burczyk@sigmapoint.pl" }
  s.platform     = :ios, '7.0'
  s.source_files  = 'NetworkLib', 'NetworkLib/**/*.{h,m}'
  s.public_header_files = 'NetworkLib/**/*.h'
  s.resources    = "NetworkLib/*.png"
  s.framework    = 'SystemConfiguration'
  s.requires_arc = true

  s.dependency 'AFNetworking'

end  

You don't need to fill all the field right away, but you should look at s.source_files, s.public_header_files, s.resources to match your project paths and optionally s.framework. The last one defines system dependencies for our lib so if your lib depends e.g. on SystemConfiguration.framework you should list it here.
Finally in s.dependency 'AFNetworking' you define all dependencies from Podfile.

Just to be clear, Podfile and its dependencies is used when you build your library alone. NetworkLib.podspec is used when you want to include library as dependency elsewhere.

You also might have noticed that I removed s.source entry. It's just because my lib is private and I don't want to put it on the Internet.

  • Write some code!

Make sure you closed previous project and opened it from .xcworkspace file otherwise you will get errors about missing libPod library

Let's write a function that downloads info about all your github repos.
The URL for that is https://api.github.com/users/<username>/repos.

NetworkLib.h:

#import <Foundation/Foundation.h>

#import "AFNetworking.h"

@interface NetworkLib : NSObject

- (void)getGithubReposForUser:(NSString*)user withSuccess:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure;

@end

NetworkLib.m:

#import "NetworkLib.h"

@implementation NetworkLib

- (void)getGithubReposForUser:(NSString*)user withSuccess:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure
{
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    [manager GET:[NSString stringWithFormat:@"https://api.github.com/users/%@/repos", user] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        success(responseObject);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        failure(error);
    }];
}

@end

Let's write some tests (the dummy one for now):

#import <XCTest/XCTest.h>
#import "NetworkLib.h"

@interface NetworkLibTests : XCTestCase

@end

@implementation NetworkLibTests

- (void)testGetReposForBurczyk
{
    NetworkLib *nl = [[NetworkLib alloc] init];
    XCTAssertNotNil(nl, @"");
}

@end

When you hit Cmd+U you will probably get an error in NetworkLib.h file:
AFNetworking.h file not found error

It's perfectly normal and is just caused by default configuration where CocoaPods library is not linked for a test phase. Let's fix that!
Navigate to your project blue icon and in Configurations section select Pods-NetworkLib in Based Configuration File column:

Changed pods configuration for tests

Ok, now the test should pass, so we can improve it. We have an asynchronous method to test, so we have to use some trick to wait for the results, otherwise test will end before we even get the results. I used helper written by @Holtwick on stackoverflow.

Full test file looks like this:

#import <XCTest/XCTest.h>
#import "NetworkLib.h"

@interface NetworkLibTests : XCTestCase

@end

@implementation NetworkLibTests

- (void)testGetReposForBurczyk
{
    __block id JSON;

    hxRunInMainLoop(^(BOOL *done) {
        NetworkLib *nl = [[NetworkLib alloc] init];
        [nl getGithubReposForUser:@"burczyk" withSuccess:^(id responseObject) {
            NSLog(@"Response: %@", responseObject);
            JSON = responseObject;
            *done = YES;
        } failure:^(NSError *error) {
            *done = YES;
        }];
    });

    XCTAssertNotNil(JSON, @"");
}

- (void)testGetRepoForNotExistingUser
{
    __block id JSON;

    hxRunInMainLoop(^(BOOL *done) {
        NetworkLib *nl = [[NetworkLib alloc] init];
        [nl getGithubReposForUser:@"burczyk1234567890" withSuccess:^(id responseObject) {
            NSLog(@"Response: %@", responseObject);
            JSON = responseObject;
            *done = YES;
        } failure:^(NSError *error) {
            *done = YES;
        }];
    });

    XCTAssertNil(JSON, @"");
}

// Wrapper to test async methods: http://stackoverflow.com/questions/2162213/how-to-unit-test-asynchronous-apis
static inline void hxRunInMainLoop(void(^block)(BOOL *done)) {  
    __block BOOL done = NO;
    block(&done);
    while (!done) {
        [[NSRunLoop mainRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow:.1]];
    }
}

@end

It tests one success and one failure scenario.

In this moment we can assume we have fully functional and tested library, so let's use it in real app.

Use your library in iOS application as CocoaPods dependency

  • Create new iOS app, name it ReposBrowser and place it on the same level as original library.
MacBook Pro:Projects kamil$ ls  
NetworkLib    ReposBrowser  
  • Create Podfile inside by running pod init and open it. The magic line you have to enter to add dependency to local lib is:

pod 'NetworkLib', :path => '../NetworkLib'

If you configured NetworkLib.podspec file correctly you should see similar output after typing pod install:

MacBook Pro:ReposBrowser kamil$ pod install  
Analyzing dependencies  
Fetching podspec for `NetworkLib` from `../NetworkLib`  
Downloading dependencies  
Installing AFNetworking (2.1.0)  
Installing NetworkLib (0.0.1)  
Generating Pods project  
Integrating client project  
[!] From now on use `ReposBrowser.xcworkspace`.
MacBook Pro:ReposBrowser kamil$  

Moreover in Xcode Pods project browser you should see Development Pods directory with Network Lib inside!

Development pods

  • Use lib in your code, e.g.
#import "ViewController.h"
#import "NetworkLib.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (weak, nonatomic) IBOutlet UITextView *textView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
}

- (IBAction)showRepos:(UIButton *)sender {
    NetworkLib *nl = [[NetworkLib alloc] init];
    [nl getGithubReposForUser:_textField.text withSuccess:^(id responseObject) {
        _textView.text = [NSString stringWithFormat:@"%@", responseObject];
    } failure:^(NSError *error) {
        _textView.text = @"Error occured";
    }];

    [_textField resignFirstResponder];
}

@end

And everything should work just fine :)

You can find sample projects from examples above under links:
NetworkLib

ReposBrowser

What's next?

In second part of this tutorial I will show you how to automatically build a static library .a file and export it with corresponding header .h files so you can distribute your lib without open sourcing it.

Third part will cover Continuous Integration with Jenkins.

comments powered by Disqus