BMCore framework for iOS

BMCore framework for iOS

Released 4 years ago , Last update 4 years ago

A framework offering core functionality for iOS applications such as HTTP, networking, service management, security, helper methods for Foundation classes, proxying, caching and threading.

Overview

A module with core functionality that can be used for both MacOSX (separate version coming) and iOS applications for many common tasks, such as:

Application management

  • Managing the application lifecycle and coordination of objects using an application context;
  • Managing application settings with an abstraction layer on top of NSUserDefaults;
  • Handling errors in a consistent way;

Service management

  • Centralizing execution and notifications for asynchronous (remote) calls with a service manager;
  • Concatenating services together, such as for a complex upload process which requires multiple calls;

HTTP/Networking

  • Performing all kinds of HTTP requests (GET, PUT, DELETE, POST, including multi-part) with different kinds of security (BASIC, client certificate, NTLM) using a streaming implementation (to avoid running out of memory for big files);
  • Performing caching for remote images and data with bounds on the max disk space, max memory usage and support for pinning;
  • Loading data for static files (such as images for tableview cells) asynchronously with a queuing and caching mechanism;
  • Testing network availability;

Threading

  • Handling thread safety for none thread safe objects (e.g. NSDateFormatter) using a proxy;
  • Making a mutable object immutable using a proxy;
  • By notified of the completion of tasks run in the background using NSOperations

Productivity

  • Performing security tasks including importing certificates/keys for use by the application or to store them in the keychain, asymmetric and symmetric encryption and referencing security identities for client certificate authentication;
  • Using compression to store or transmit data;
  • Easy navigation through dictionaries (such as parsed from JSON) using XPath expressions;
  • Using ordered (maintaining insertion order) and two-way dictionaries (look up both ways);
  • Using regex expressions on strings;
  • Converting between different classes of objects in a uniform way, such as data to string, date to number and vice versa;
  • Playing custom sounds (e.g. for alerts);
  • Generating GUIDs, representing currencies/numbers, converting from string to URL using proper escaping, parsing HTTP query parameters from a string and other string manipulations/conversions;

This module is part of the BMCommons framework and has been developed and used in production apps during the last five years including the BehindTheFrontDoor iPhone app.

This framework is compatible with all iOS versions from 5.0 to 7.x.

The author has over 15 years experience as software developer and a track record of many successful apps since the launch of the app store in 2008 including the Greetz iPhone app, which won the award of best mobile web store of 2012 in The Netherlands.

Contents

Here is a full list of all classes with a short description of their purpose.

Also take a look at the example code shown below and included in the download.

Pricing

14 day 14-day money-back guarantee

$349.99

Distributor License

  • Perpetual license

  • Unlimited projects

  • Can distribute code and binary products

  • Commercial use

  • 6 months support

Documentation

The full documentation is installable as an XCode docset as part of the free download (click View Demo) and available in HTML here.

Here is an overview of all classes in BMCore module.

For a description of the BMService framework see this link.

Please ask if things are unclear or need to be clarified with examples.

Setup / installation

To install:

  • Unzip the downloaded zip-file which contains the BMCommons frameworks, documentation and example code
  • Copy the docset included in the Documentation directory to your XCode docset directory as specified in the README.md file included in the download for context sensitive documentation in XCode.
  • Add the BMCore.framework from the Frameworks directory to your project's target.
  • A license key (which you will get upon purchase) is required for productional use on an iOS device. Register the license key in your AppDelegate's init method as follows:

    [[BMCore instance] registerLicenseKey:@"TheLicenseKey"];

The BMCore module additionally relies on the following dynamic frameworks/libraries which should be linked in an executable containing this module:

  • Foundation.framework
  • libicucore.dylib
  • libz.dylib
  • CoreGraphics.framework
  • SystemConfiguration.framework
  • AudioToolbox.framework
  • Security.framework
  • UIKit.framework (AppKit.framework for MacOSX)

Example usage

Application lifecycle and settings

Use BMApplicationContext to manage settings, singletons and application lifecycle in a central place.

Sub-class BMApplicationContext:

@interface CustomApplicationContext : BMApplicationContext

@end

//Sample sub-class of BMApplicationContext. There should be no GUI code in here, that belongs in the AppDelegate
@implementation CustomApplicationContext

- (NSArray *)settingsObjectClasses {
    //This should return an array of classes implementing the protocol BMSettingsObject.
    //Saving and loading of settings to NSUserDefaults is handled automatically.
    return @[[ApplicationSettings class]];
}


- (void)initialize {
    //Perform initialization

    /*

     For example:

    [Flurry setSecureTransportEnabled:YES];
    [Flurry setAppVersion:[self fullAppVersion]];
    [Flurry startSession:FLURRY_API_KEY];

    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);

    [TestFlight takeOff:TESTFLIGHT_TEAM_TOKEN];
#if DEBUG
    [TestFlight setDeviceIdentifier:[[UIDevice currentDevice] uniqueIdentifier]];
#endif

     */

    //Perform custom initialization on first run, or the first run of the current version of the app
    if (self.settings.isFirstRun) {

    } else if (self.settings.isFirstRunForCurrentVersion) {

    }

    [super initialize];
}

- (void)delayedInitialization {
    //Perform initialization that should be done asynchronously after application did finish launching.


    //Always call super as last statement
    [super delayedInitialization];
}

- (void)activate {
    /**

     For example enable timers, etc

     */
    [super activate];
}

- (void)deactivate {
    /**

     For example disable timers, etc

     */
    [super deactivate];
}

@end

Tie the application context to the app delegate:

@implementation AppDelegate {
    BMApplicationContext *_applicationContext;
}

- (id)init {
    if ((self = [super init])) {
        _applicationContext = (BMApplicationContext *)[[self applicationContextClass] sharedInstance];
    }
    return self;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    [_applicationContext initialize];

    //More initialization...

    return YES;
}


- (void)applicationWillResignActive:(UIApplication *)application {
    [_applicationContext deactivate];
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [_applicationContext activate];
}


- (void)applicationWillTerminate:(UIApplication *)application {
    [_applicationContext terminate];
}

@end

Implement a settings object for your application (which is automatically synched with NSUserDefaults):

@interface ApplicationSettings : BMAbstractSettingsObject

@property (nonatomic, retain) NSDate *lastLoginDate;
@property (nonatomic, retain) NSString *username;
@property (nonatomic, retain) NSString *password;

@end

//This class demonstrates how to sub-class BMAbstractSettingsObject to save/load settings for your app automatically. Not that the saving/loading to and from NSUserDefaults is all done automatically.
@implementation ApplicationSettings {
    BMKeychainItemWrapper *_keychainItem;
}

@synthesize lastLoginDate;

/**
 If you have a generic "restore settings to defaults" button, you can devide settings object to allow certain settings to be restorable, while others are not.
 */
+ (BOOL)allowRestoreToDefaults {
    return NO;
}

//This method should return the default values for the properties returned by valuePropertiesArray
+ (NSArray *)defaultValuesArray {
    return @[[NSNull null], @"http://www.someserver.com/service"];
}

//Properties to store in NSUserDefaults
+ (NSArray *)valuePropertiesArray {
    return @[@"lastLoginDate", @"serverURL"];
}

//Reset is called automatically the first time after a clean install: use this to wipe the keychain for example, because the keychain is not automatically cleared if the user removes an application.
- (void)reset {
    [super reset];

    [self.keychainItem resetKeychainItem];
}

- (void)dealloc {
    //Release the custom ivars
    [_keychainItem release];

    //The properties registered through valuePropertiesArray are automatically deallocated by the super class
    [super dealloc];
}

//Username and password are stored separately in the keychain for security, NSUserDefaults is not secure
- (void)setUsername:(NSString *)username andPassword:(NSString *)password {
    [self.keychainItem setObject:[BMStringHelper filterNilString:username] forKey:(id) kSecAttrAccount];
    [self.keychainItem setObject:[BMStringHelper filterNilString:password] forKey:(id) kSecValueData];
}

- (NSString *)username {
    return [self.keychainItem objectForKey:(id) kSecAttrAccount];
}

- (NSString *)password {
    return [self.keychainItem objectForKey:(id) kSecValueData];
}

- (BMKeychainItemWrapper *)keychainItem {
    if (_keychainItem == nil) {
        _keychainItem = [[BMKeychainItemWrapper alloc] initWithIdentifier:@"" accessGroup:nil];
    }
    return _keychainItem;
}

@end

HTTP requests

Perform a multi-part upload:

 NSURL *url = [NSURL URLWithString:@"http://posttestserver.com/post.php?dir=example"];

//Upload a test PNG file
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"AppleLogo" withExtension:@"png"];

NSArray *contentParts = @[
                          //Some text parameter
                          [BMHTTPContentPart contentPartWithName:@"someParam" andValue:@"someValue"],

                          //Using a file URL will create an input stream for the file to upload. It will not load all data in memory at once,
                          //so uploading big files is not a problem.
                          [BMHTTPContentPart contentPartWithName:@"submitted" fileURL:fileURL]
                          ];
 _httpRequest = [[BMHTTPRequest alloc] initMultiPartPostRequestWithUrl:url
                                                        contentParts:contentParts
                                                  customHeaderFields:nil
                                                            userName:@"someuser"  //used for BASIC or NTLM authentication
                                                            password:@"somepassword"  //used for BASIC or NTLM authentication
                                                            delegate:self];   
[_httpRequest send];

Perform a streaming download for a big file:

//Use any URL to test streaming download
NSURL *url = [NSURL URLWithString:@"http://cdimage.debian.org/debian-cd/7.1.0/amd64/iso-cd/debian-7.1.0-amd64-netinst.iso"];
//Optional query parameters to append to the url
NSDictionary *params = nil;

_httpRequest = [[BMHTTPRequest alloc] initGetRequestWithUrl:url
                                                 parameters:params
                                         customHeaderFields:nil
                                                   userName:@"someuser"
                                                   password:@"somepassword"
                                                   delegate:self];

NSInputStream *inputStream = [_httpRequest inputStreamForConnection];
inputStream.delegate = self;

[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[inputStream open];

Use client certificate authentication by first importing the certificate data from a p12 file:

NSString *keyFilePath = [[NSBundle mainBundle] pathForResource:@"keypair" ofType:@"p12"];
NSError *error = nil;
NSData *identityRefData = [BMSecurityHelper importP12DataFromFile:keyFilePath usingPassword:@"somepassword" withError:&error];

_httpRequest = [[BMHTTPRequest alloc] initGetRequestWithUrl:url
                                                 parameters:nil
                                         customHeaderFields:nil
                                                   userName:nil
                                                   password:nil
                                                   delegate:self];
//Optionally allow self-signed HTTPS certificates for testing
_httpRequest.shouldAllowSelfSignedCert = YES;
_httpRequest.clientIdentityRef = identityRefData;
[_httpRequest send];

BMService framework

A sample HTTP service for shortening URLs using BitLy:

@interface BitlyShortenURLService : BMHTTPService
@end

/**
 This example service calls the bitly shorten URL service to return a shortened version of a long URL.
 */
@implementation BitlyShortenURLService

@synthesize longUrl = _longUrl;

- (void)dealloc {
    self.longUrl = nil;
    [super dealloc];
}

/**
 @brief Returns the request to execute for this service.

 BMHTTPService calls this method when it is ready to instantiate an HTTP request.
 This method should create a proper BMHTTPRequest with URL, parameters, basic auth, etc set as needed.
 */
- (BMHTTPRequest *)requestForServiceWithError:(NSError **)error {

    if (self.longUrl == nil) {
        //Return nil to signal a validation error
        if (error) {
            *error = [BMErrorHelper errorForDomain:BM_ERROR_DOMAIN_SERVICE
                                              code:BM_ERROR_INVALID_DATA
                                       description:@"No long url specified"];
        }
        return nil;
    }

    NSString *accessToken = @"someaccesstoken";
    NSString *urlString = @"https://api-ssl.bitly.com/v3/shorten";

    return [[[BMHTTPRequest alloc] initGetRequestWithUrl:[NSURL URLWithString:urlString]
                                             parameters:@{
                                                        @"access_token" : accessToken,
                                                        @"longUrl" : self.longUrl
                                                        }
                                     customHeaderFields:nil
                                               userName:nil
                                               password:nil
                                               delegate:nil] autorelease];
}

/**
 @brief Extracts a result object from the response (or null in case of error).
 */
- (id)resultFromRequest:(BMHTTPRequest *)theRequest {

    NSLog(@"Response: %@", theRequest.reply);

    //Parse the response and return the shortened url: this is the result for the service
    NSDictionary *responseDictionary = [theRequest.reply JSONValue];

    return [responseDictionary valueForXPath:@"/data/url" withClass:[NSString class]];
}

/**
 @brief Extracts/creates an error object from the response.
 */
- (NSError *)errorFromRequest:(BMHTTPRequest *)theRequest {
    //This method is called if the HTTP response is unsuccessful (not HTTP OK, 200) or when resultFromRequest returns nil

    NSDictionary *responseDictionary = [theRequest.reply JSONValue];

    NSNumber *statusCode = [responseDictionary valueForXPath:@"/status_code" withClass:[NSNumber class]];
    NSString *statusText = [responseDictionary valueForXPath:@"/status_txt" withClass:[NSString class]];

    if (statusCode != nil && statusText != nil) {
        return [BMErrorHelper errorForDomain:BM_ERROR_DOMAIN_SERVICE code:statusCode.intValue description:statusText];
    } else {
        //Could not parse error, the service will fail with a generic error instead
        return nil;
    }

}

@end

A BMCompositeService implementation to act as a facade for multiple services that have to be coordinated:

@interface SessionAwareService : BMCompositeService
@end

@implementation SessionAwareService

@synthesize wrappedService = _wrappedService;
@synthesize session = _session;

- (id)initWithService:(id <BMService>)service {
    if ((self = [super initWithDelegate:service.delegate])) {
        service.delegate = self;
        _wrappedService = [service retain];
    }
    return self;
}

- (void)cancel {
    //Super cancels self.currentService;
    [super cancel];
}

- (void)dealloc {
    BM_RELEASE_SAFELY(_wrappedService);
    BM_RELEASE_SAFELY(_session);
    [super dealloc];
}

#pragma mark -
#pragma mark Overridden superclass methods

- (NSString *)instanceIdentifier {
    //Return the instance identifier of the wrapped service so this class is fully transparent to the caller/delegate
    return _wrappedService.instanceIdentifier;
}

- (NSString *)classIdentifier {
    //Return the class identifier of the wrapped service so this class is fully transparent to the caller/delegate
    return _wrappedService.classIdentifier;
}

#pragma mark -
#pragma mark BMServiceDelegate

/**
 * Implement to act on successful completion of a service.
 */
- (void)service:(id)theService succeededWithResult:(id)result {
    if (theService == self.wrappedService) {
        NSLog(@"Wrapped service succeeded with result: %@", result);
        //The wrapped service succeeded: just call super which will call serviceSucceededWithResult:
        [super service:theService succeededWithResult:result];
    } else if ([[theService classIdentifier] isEqual:[OpenSessionService classIdentifier]]){
        NSLog(@"Session was opened successfully");
        self.session = result;
        //Successful login: execute the wrapped service now that we have a session

        //Normally you would have to do something with the session ticket,
        //e.g. set the authorization header in the request or something or supply it to the wrapped service,
        //but this is just an example.

        [self executeService:self.wrappedService];
    }
}

- (void)service:(id <BMService>)theService failedWithError:(NSError *)error {
    [super service:theService failedWithError:error];
}

- (BOOL)executeWithError:(NSError **)error {
    if ([self isSessionValid]) {
        //Session is valid: just execute the wrapped service
        [self executeService:self.wrappedService];
    } else {
        //Session is not valid: first open a session
        OpenSessionService *openSessionService = [OpenSessionService new];
        openSessionService.username = @"someuser";
        openSessionService.password = @"somepassword";
        [self executeService:openSessionService];
        [openSessionService release];
    }
    return YES;
}

- (BOOL)isSessionValid {
    return self.session != nil && [self.session.expirationDate timeIntervalSinceNow] > 0;
}

@end

Use BMServiceManager to execute and manage services:

//Get the shared service manager
BMServiceManager *serviceManager = [BMServiceManager sharedInstance];

//Create a dummy test service
DummyService1 *dummyService1 = [[DummyService1 alloc] init];

//This should explicitly be enabled to be able to send a service to background. Default is NO: then sendToBackground has no effect.
dummyService1.sendToBackgroundSupported = YES;

//Execute the service, delegate will receive call backs
NSString *instanceIdentifier = [serviceManager performService:dummyService1 withDelegate:self];

//Create and execute another service with a separate class identifier
DummyService2 *dummyService2 = [[DummyService2 alloc] init];

instanceIdentifier = [serviceManager performService:dummyService2 withDelegate:self];

NSArray *services = [serviceManager activeServicesOfClass:[DummyService1 classIdentifier] forDelegate:self];

services = [serviceManager activeServicesOfClass:[DummyService2 classIdentifier] forDelegate:self];

//Cancel service 2
[serviceManager cancelServiceInstance:dummyService2.instanceIdentifier];

//Send service 1 to background
[serviceManager sendServiceInstanceToBackground:dummyService1.instanceIdentifier];

Asynchronous data loading

A sample showing how to use BMAsyncDataLoader to aynchronously load data (such as images):

- (void)performAsyncDataLoading {

    [BMAsyncDataLoader setMaxConnections:2];

    BMAsyncDataLoader *dataLoader = [[BMAsyncDataLoader alloc] initWithURLString:@"http://www.someserver.com/somefile.png"];
    dataLoader.delegate = self;
    dataLoader.maxRetryCount = 2;
    dataLoader.connectionTimeOut = 5.0;
    dataLoader.ignoreCache = NO;
    dataLoader.storeToCache = YES;
    [dataLoader startLoadingWithPriority:YES];
}

#pragma mark - BMAsyncDataLoaderDelegate

/**
Callback sent on completion.

If error == nil the loading was successful and the [BMAsyncDataLoader object] property contains the data (or converted data) loaded.
*/
- (void)asyncDataLoader:(BMAsyncDataLoader *)dataLoader didFinishLoadingWithError:(NSError *)error {
    if (error) {
        NSLog(@"Data could not be loaded: %@", error);
    } else {
        //Do something with the data.
        NSLog(@"Loaded successfully with mimeType: %@, data: %@", dataLoader.mimeType, dataLoader.object);
    }
}

For more example code view the examples in the free download (click View Demo) which is fully functional in the simulator and contains examples for the different modules part of the BMCommons framework.

For device usage you need a license key which you will get upon purchase.

License

Three licenses are offered for this module:

  • Application License: valid for a single application (binary code only);
  • Developer License: valid for either 5 applications with generic bundle identifiers or for all applications within your company, with application bundle identifiers matching to com.yourcompany.* (binary code only);
  • Distributor License: valid for unlimited applications, including the source code. Gives you right to distribute the source code or binary as part of a bigger project, but prohibits reselling the library itself.

NOTE: Only the distributor package contains the source code for this module. Other packages include the binary library only.

The demo download includes all modules of the BMCommons framework including example code with unlimited functionality in the iOS simulator. Running on an actual device requires a valid license key to be registered as follows:

[[BMCore instance] registerLicenseKey:@"TheLicenseKey"];

License keys may be managed via https://license.behindmedia.com. You may also request a trial there.

1 license From » $49.99 View Licenses

Get A Quote

What do you need?
  • Custom development
  • Integration
  • Customization / Reskinning
  • Consultation
When do you need it?
  • Soon
  • Next week
  • Next month
  • Anytime

Thanks for getting in touch!

Your quote details have been received and we'll get back to you soon.


Or enter your name and Email
No comments have been posted yet.