Installation and usage of iOS & Swift SDKs with Objective-C

Comprehensive guide to using Filestack's iOS SDK in Objective-C: Learn to pick files, enable image editing, and perform transformations..

Overview

In this tutorial, we’ll create a fresh iOS app that allows us to pick files, edit images using the built-in image editor, and transform images using Filestack’s remote API. We will rely on CocoaPods to handle the external dependency on Filestack framework and all its sub-dependencies.

IMPORTANT: This tutorial targets Objective-C. If you would rather prefer to use Swift, please check our homologous tutorial Installation and usage of the iOS & Swift SDKs instead.

With that said, let’s get started!

Installation

  1. Open Xcode and create a new project (File -> New Project), select iOS as the platform and Tabbed App as the template:

  2. On the next step, name your project Filestack-Tutorial, leaving all other settings as default:

  3. We will be creating our views programmatically so, delete (using Move to Trash) the following files from the project hierarchy:

    • FirstViewController.h
    • FirstViewController.m
    • SecondViewController.h.
    • SecondViewController.m.
    • Main.storyboard
  4. Now click on the project’s root, select the Filestack-Tutorial target, find the Signing section and enable Automatic signing, and set up a Team:

  5. Change the Main Interface setting from Main to an empty value (we will be creating our UI programmatically). For Device Orientation make sure only Portrait is enabled:

  6. Now go to the Info tab, expand URL Types and click the plus (+) button. Set the identifier to com.filestack.tutorial and URL Schemes to tutorial.

  7. Add the following keys to Custom iOS Target Properties:

KeyValue
NSPhotoLibraryUsageDescriptionThis demo needs access to the photo library.
NSCameraUsageDescriptionThis demo needs access to the camera.

  1. Right-click each of the following 2 images and save them locally on your Mac. We will need them in our project:

  2. Now create a new group inside Filestack-Tutorial called Images and add the images you just downloaded to it, making sure Copy items if needed is checked. File hierarchy should look like this:

10. Finally, let’s configure an Objective-C bridging header so we can use Swift frameworks from our Objective-C project. Right-click Filestack-Tutorial on the Project Navigator, choose New File…, and select Swift File. The name of the file is not important, just make sure you choose Create Bridging Header when asked if you would like to configure an Objective-C bridging header. Once done, you can delete the file with swift extension that was created.

Setup

In case CocoaPods is not already present in your system, you will need to install it using RubyGems:

gem install cocoapods

After that, in your project’s root run:

pod init

A new file called Podfile will be created. Open the file with your favorite text editor and enter the following:

platform :ios, '11.0'

target 'Filestack-Tutorial' do
  use_frameworks!
  pod 'Filestack', '~> 2.1'
end

Now we are ready to setup our project to use CocoaPods with the pods we need. For that we run the following:

pod install --repo-update

CocoaPods will generate a new Filestack-Tutorial.xcworkspace that we will be using from now on to work on our project. If Filestack-Tutorial.xcodeproj is still open in Xcode, please close it and open Filestack-Tutorial.xcworkspace instead.

In the new workspace we just created, right-click Filestack-Tutorial on the Project Navigator, choose New File…, and select Cocoa Touch Class. Name your class FilestackSetup and make it a subclass of NSObject.

Open FilestackSetup.h and add the following contents to it, making sure to set your API key and app secret accordingly:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

// Set your app's URL scheme here.
static NSString *appURLScheme = @"tutorial";
// Set your Filestack's API key here.
static NSString *filestackAPIKey = @"YOUR-API-KEY-HERE";
// Set your Filestack's app secret here.
static NSString *filestackAppSecret = @"YOUR-APP-SECRET-HERE";

@class FSFilestackClient;


@interface FilestackSetup : NSObject

@property(nonatomic, strong) FSFilestackClient *client;

+ (FilestackSetup *)sharedSingleton;

@end

NS_ASSUME_NONNULL_END

Now open FilestackSetup.m and add the following contents:

#import "FilestackSetup.h"
@import FilestackSDK;
@import Filestack;

@implementation FilestackSetup

-(instancetype)init {
    self = [super init];

    if (self) {
        FSPolicyCall policyCall =
            FSPolicyCallPick | FSPolicyCallRead | FSPolicyCallStat |
            FSPolicyCallWrite | FSPolicyCallWriteURL | FSPolicyCallStore |
            FSPolicyCallConvert | FSPolicyCallRemove | FSPolicyCallExif;

        FSPolicy *policy = [[FSPolicy alloc] initWithExpiry:NSDate.distantFuture
                                                       call:policyCall];

        FSSecurity *security = [[FSSecurity alloc] initWithPolicy:policy
                                                        appSecret:filestackAppSecret
                                                            error:nil];

        FSConfig *config = [FSConfig new];

        config.appURLScheme = appURLScheme;
        config.imageURLExportPreset = FSImageURLExportPresetCurrent;
        config.maximumSelectionAllowed = 10;

        config.availableCloudSources = @[FSCloudSource.dropbox,
                                         FSCloudSource.googleDrive,
                                         FSCloudSource.googlePhotos,
                                         FSCloudSource.customSource];

        config.availableLocalSources = @[FSLocalSource.camera,
                                         FSLocalSource.photoLibrary,
                                         FSLocalSource.documents];

        self.client = [[FSFilestackClient alloc] initWithApiKey:filestackAPIKey
                                                       security:security
                                                         config:config
                                                          token:nil];
    }

    return self;
}

+ (FilestackSetup *)sharedSingleton
{
    static FilestackSetup *sharedSingleton;

    @synchronized(self)
    {
        if (!sharedSingleton)
            sharedSingleton = [FilestackSetup new];

        return sharedSingleton;
    }
}

@end

Open AppDelegate.m and add the following imports:

#import "FilestackSetup.h"

Replace your existing - application:didFinishLaunchingWithOptions: with:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [FilestackSetup sharedSingleton];

    return YES;
}

Setup UI

Our demo app is going to present a tab view controller containing 2 view controllers:

  • PickerViewController — we’ll use this view to present the picker
  • TransformImagesViewController — we’ll use this view to transform an image using Filestack’s transformation API

OK, so let’s create our 2 view controllers first…

Right-click Filestack-Tutorial on the Project Navigator, choose New File… and select Cocoa Touch Class. Name your class PickerViewController and make it a subclass of UIViewController.

Open PickerViewController.m and add:

#import "PickerViewController.h"
#import "FilestackSetup.h"
@import Filestack;
@import FilestackSDK;

@interface PickerViewController () <FSPickerNavigationControllerDelegate>

@property(nonatomic, strong) UIButton *presentPickerButton;

@end

@implementation PickerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.presentPickerButton = [UIButton buttonWithType:UIButtonTypeSystem];

    [self.presentPickerButton setTitle:@"Present Picker"
                              forState:UIControlStateNormal];

    [self.presentPickerButton addTarget:self
                                 action:@selector(presentPicker:)
                       forControlEvents:UIControlEventTouchUpInside];

    self.presentPickerButton.translatesAutoresizingMaskIntoConstraints = NO;

    [self.view addSubview:self.presentPickerButton];

    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.presentPickerButton
                                                          attribute:NSLayoutAttributeCenterX
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.view
                                                          attribute:NSLayoutAttributeCenterX
                                                         multiplier:1
                                                           constant:0]];

    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.presentPickerButton
                                                          attribute:NSLayoutAttributeCenterY
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.view
                                                          attribute:NSLayoutAttributeCenterY
                                                         multiplier:1
                                                           constant:0]];
}

- (IBAction)presentPicker:(id)sender {}

@end

Right-click Filestack-Tutorial on the Project Navigator, choose New File… and select Cocoa Touch Class. Name your class TransformImagesViewController and make it a subclass of UIViewController.

Open TransformImagesViewController.m and add:

#import "TransformImagesViewController.h"
#import "FilestackSetup.h"
@import FilestackSDK;
@import Filestack;

@interface TransformImagesViewController ()

@property(nonatomic, strong) NSURL *originalImageURL;
@property(nonatomic, strong) NSURL *placeholderImageURL;

@property(nonatomic, strong) UILabel *originalImageLabel;
@property(nonatomic, strong) UIImageView *originalImageView;

@property(nonatomic, strong) UIImageView *transformedImageView;
@property(nonatomic, strong) UIButton *transformImageButton;

@property(nonatomic, strong) UIStackView *stackView;

@end

@implementation TransformImagesViewController

- (NSURL *)originalImageURL {
    if (!_originalImageURL) {
         _originalImageURL = [NSBundle.mainBundle URLForResource:@"original" withExtension:@"jpg"];
    }

    return _originalImageURL;
}

- (NSURL *)placeholderImageURL {
    if (!_placeholderImageURL) {
        _placeholderImageURL = [NSBundle.mainBundle URLForResource:@"placeholder" withExtension:@"png"];
    }

    return _placeholderImageURL;
}

-(UILabel *)originalImageLabel {
    if (!_originalImageLabel) {
        // Setup original image label
        UILabel *label = [UILabel new];

        label.text = @"Original Image";
        label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
        label.textColor = UIColor.lightGrayColor;

        [label setContentCompressionResistancePriority:UILayoutPriorityRequired
                                               forAxis:UILayoutConstraintAxisVertical];

        _originalImageLabel = label;
    }

    return _originalImageLabel;
}

-(UIImageView *)originalImageView {
    if (!_originalImageView) {
        // Setup original image view
        UIImage *originalImage = [UIImage imageWithContentsOfFile:self.originalImageURL.path];
        UIImageView *imageView = [[UIImageView alloc] initWithImage:originalImage];

        imageView.contentMode = UIViewContentModeScaleAspectFit;

        NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:imageView
                                     attribute:NSLayoutAttributeHeight
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:nil
                                     attribute:NSLayoutAttributeHeight
                                    multiplier:1
                                      constant:220];

        [constraint setActive:YES];

        _originalImageView = imageView;
    }

    return _originalImageView;
}

-(UIImageView *)transformedImageView {
    if (!_transformedImageView) {
        // Setup transformed image view
        UIImage *placeholderImage = [UIImage imageWithContentsOfFile:self.placeholderImageURL.path];
        UIImageView *imageView = [[UIImageView alloc] initWithImage:placeholderImage];

        imageView.contentMode = UIViewContentModeScaleAspectFit;

        NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:imageView
                                                                      attribute:NSLayoutAttributeHeight
                                                                      relatedBy:NSLayoutRelationEqual
                                                                         toItem:nil
                                                                      attribute:NSLayoutAttributeHeight
                                                                     multiplier:1
                                                                       constant:220];

        [constraint setActive:YES];

        _transformedImageView = imageView;
    }

    return _transformedImageView;
}

-(UIButton *)transformImageButton {
    if (!_transformImageButton) {
        // Setup transformed image button
        UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];

        [button setTitle:@"Transform Image"
                forState:UIControlStateNormal];

        [button addTarget:self
                   action:@selector(transformImage:)
         forControlEvents:UIControlEventTouchUpInside];

        _transformImageButton = button;
    }

    return _transformImageButton;
}

- (UIStackView *)stackView {
    if (!_stackView) {
        // Setup stack view
        UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[self.originalImageLabel,
                                                                                 self.originalImageView,
                                                                                 self.transformImageButton,
                                                                                 self.transformedImageView
                                                                                 ]];

        stackView.axis = UILayoutConstraintAxisVertical;
        stackView.alignment = UIStackViewAlignmentCenter;
        stackView.distribution = UIStackViewDistributionFillProportionally;
        stackView.spacing = 22;
        stackView.translatesAutoresizingMaskIntoConstraints = NO;

        _stackView = stackView;
    }

    return _stackView;
}

- (void)viewDidLoad {
    // Add stack view to view hierarchy
    [self.view addSubview:self.stackView];
}

- (void)viewDidAppear:(BOOL)animated {
    // Setup stack view constraints
    NSDictionary<NSString *, id> *views = @{@"stackView": self.stackView};

    NSArray *h = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-22-[stackView]-22-|"
                                                         options:0
                                                         metrics:nil
                                                           views:views];

    NSArray *w = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[stackView]-bottom-|"
                                                         options:0
                                                         metrics:@{@"top": @(self.view.safeAreaInsets.top + 22),
                                                                   @"bottom": @(self.view.safeAreaInsets.bottom + 22)}
                                                           views:views];

    // Remove existing view constraints
    [self.view removeConstraints:self.view.constraints];
     // Add new view constraints
    [self.view addConstraints:h];
    [self.view addConstraints:w];

    [super viewDidAppear:animated];
}

-(IBAction)transformImage:(id)sender {}

@end

Once we have the 2 view controllers set up, let’s define our tab bar view controller.

Right-click Filestack-Tutorial on the Project Navigator, choose New File… and select Cocoa Touch Class. Name your class TabBarViewController and make it a subclass of UITabBarController.

Open TabBarViewController.m and add:

#import "TabBarController.h"
#import "PickerViewController.h"
#import "TransformImagesViewController.h"

@implementation TabBarController

-(void)viewDidLoad {
    self.view.backgroundColor = UIColor.whiteColor;

    PickerViewController *pickerViewController = [PickerViewController new];

    pickerViewController.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"Image Picker"
                                                                    image:[UIImage imageNamed:@"first"]
                                                                      tag:0];

    TransformImagesViewController *transformImagesViewController = [TransformImagesViewController new];

    transformImagesViewController.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"Transform Image"
                                                                             image:[UIImage imageNamed:@"second"]
                                                                               tag:0];

    self.viewControllers = @[pickerViewController, transformImagesViewController];
}

@end

Finally, open AppDelegate.m and replace - application:didFinishLaunchingWithOptions: with:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [FilestackSetup sharedSingleton];

    // Added code — we set our TabBarController as the window's root view controller
    // and make window key and visible.
    self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[TabBarController new]];
    [self.window makeKeyAndVisible];

    return YES;
}

We are also going to add a helper function to simplify the alert presentation. Right-click Filestack-Tutorial on the Project Navigator, choose New Group, and name it Categories. Now, right-click Categories and choose New File… and select Objective-C. Name it PresentAlert and set class to UIViewController.

Open UIViewController+PresentAlert.h and add:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIViewController (PresentAlert)

-(void)presentAlert:(NSString*)title message:(NSString *)message;

@end

NS_ASSUME_NONNULL_END

Now open UIViewController+PresentAlert.m and add:

#import "UIViewController+PresentAlert.h"

@implementation UIViewController (PresentAlert)

-(void)presentAlert:(NSString*)title message:(NSString *)message {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title
                                                                             message:message
                                                                      preferredStyle:UIAlertControllerStyleAlert];

    [alertController addAction:[UIAlertAction actionWithTitle:@"OK"
                                                       style:UIAlertActionStyleDefault
                                                     handler:nil]];

    [self presentViewController:alertController
                       animated:NO
                     completion:nil];
}

@end

Our UI and helper code are now ready and all left to do is to add the handling for - presentPicker: and - transformImage:. That’s exactly what we will be doing in the next steps.

Picker integration

Open PickerViewController.m and add the following implementation to - presentPicker::

- (IBAction)presentPicker:(id)sender {
    FSFilestackClient *fsClient = [FilestackSetup sharedSingleton].client;

    if (!fsClient)
        return;

    // Store options for your uploaded files.
    // Here we are saying our storage location is S3 and access for uploaded files should be public.
    FSStorageOptions *storeOptions = [[FSStorageOptions alloc] initWithLocation:FSStorageLocationS3
                                                                         access:FSStorageAccessPublic];
    // Instantiate picker by passing the `StorageOptions` object we just set up.
    FSPickerNavigationController *picker = [fsClient pickerWithStoreOptions:storeOptions];
    // Set our view controller as the picker's delegate.
    picker.pickerDelegate = self;
    // Finally, present the picker on the screen.
    [self presentViewController:picker
                       animated:YES
                     completion:nil];
}

In order to receive notifications from the picker (e.g., when files are uploaded to the storage location), we’ll want our view controller to implement the PickerNavigationControllerDelegate protocol. We already set up our view controller as the picker’s delegate in our code, so all that is left to do is to implement the protocol.

First, make sure to add conformance to FSPickerNavigationControllerDelegate to PickerViewController:

@interface PickerViewController () <FSPickerNavigationControllerDelegate>

Then add the implementation:

#pragma mark - FSPickerNavigationControllerDelegate Implementation

// A file was picked from a cloud source
- (void)pickerStoredFileWithPicker:(FSPickerNavigationController * _Nonnull)picker response:(FSStoreResponse * _Nonnull)response {
    [picker dismissViewControllerAnimated:NO completion:^{
        NSString *handle = response.contents[@"handle"];
        NSError *error = response.error;

        if (handle != nil) {
            [self presentAlert:@"Success"
                       message:[NSString stringWithFormat:@"Finished storing file with handle: %@", handle]];
        } else if (error != nil) {
            [self presentAlert:@"Error"
                       message:[NSString stringWithFormat:@"Error Uploading File: %@", error.localizedDescription]];
        }
    }];
}

// A file or set of files were picked from the camera, photo library, or Apple's Document Picker
- (void)pickerUploadedFilesWithPicker:(FSPickerNavigationController * _Nonnull)picker responses:(NSArray<FSNetworkJSONResponse *> * _Nonnull)responses {
    [picker dismissViewControllerAnimated:NO completion:^{
        NSMutableArray<NSString *> *handles = [NSMutableArray arrayWithCapacity:10];
        NSMutableArray<NSString *> *errorMessages = [NSMutableArray arrayWithCapacity:10];

        for (FSNetworkJSONResponse *response in responses) {
            NSString *handle = response.json[@"handle"];
            NSError *error = response.error;

            if (handle != nil) {
                [handles addObject:handle];
            }

            if (error != nil) {
                [errorMessages addObject:error.localizedDescription];
            }
        }

        if (errorMessages.count == 0) {
            NSString *joinedHandles = [handles componentsJoinedByString:@", "];
            [self presentAlert:@"Success"
                       message:[NSString stringWithFormat:@"Finished storing files with handles: %@", joinedHandles]];
        } else {
            NSString *joinedErrors = [errorMessages componentsJoinedByString:@", "];
            [self presentAlert:@"Error Uploading File"
                       message:joinedErrors];
        }
    }];
}

Image editor integration

Wouldn’t it be nice to allow users to edit images before they are uploaded? Luckily this is a simple config setting.

Open your FilestackSetup.m and locate:

config.availableLocalSources = @[FSLocalSource.camera,
                                    FSLocalSource.photoLibrary,
                                    FSLocalSource.documents];

Add the following line right below it:

config.showEditorBeforeUpload = YES;

Now, after the user is finished picking files it will have the chance to edit any of the files that happen to be images before they are uploaded to the storage location.

Displaying and transforming files

Open the TransformImagesViewController.m file and let’s first add a helper function:

#pragma mark - Private Methods

-(NSURL *)documentURLFor:(FSFileLink *)filelink {
    NSURL *url = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
                                                      inDomains:NSUserDomainMask].firstObject;

    return [[url URLByAppendingPathComponent:filelink.handle] URLByAppendingPathExtension:@"jpg"];
}

And now add the actual implementation for - transformImage::

-(IBAction)transformImage:(id)sender {
    FSFilestackClient *fsClient = [FilestackSetup sharedSingleton].client;

    if (!fsClient)
        return;

    [self.transformImageButton setEnabled:NO];
    [self.transformImageButton setTitle:@"Uploading image..." forState:UIControlStateDisabled];

    FSStorageOptions *storeOptions = [[FSStorageOptions alloc] initWithLocation:FSStorageLocationS3
                                                                         access:FSStorageAccessPublic];

    [fsClient uploadFrom:self.originalImageURL
            storeOptions:storeOptions
useIntelligentIngestionIfAvailable:YES
                   queue:dispatch_get_main_queue()
          uploadProgress:nil
       completionHandler:^(FSNetworkJSONResponse * _Nullable response) {
           NSError *error = response.error;
           NSString *handle = response.json[@"handle"];

           if (error != nil) {
               [self presentAlert:@"Transformation Error"
                          message:error.localizedDescription];
           } else if (handle != nil) {
               [self.transformImageButton setTitle:@"Transforming image..."
                                          forState:UIControlStateDisabled];

               // Obtain Filelink for uploaded file.
               FSFileLink *uploadedFilelink = [fsClient.sdkClient fileLinkFor:handle];
               // Obtain transformable for Filelink.
               FSTransformable *transformable = [uploadedFilelink transformable];

               // Add some transformations.
               [transformable add:[[[[[FSResizeTransform new]
                                      width:220]
                                     height:220]
                                    fit:FSTransformFitCrop]
                                   align:FSTransformAlignCenter]];

               [transformable add:[[[FSRoundedCornersTransform new]
                                    radius:20]
                                   blur:0.25]];

               // Store transformed image in Filestack storage.
               [transformable storeUsing:storeOptions
                            base64Decode:NO
                                   queue:dispatch_get_main_queue()
                       completionHandler:^(FSFileLink * _Nullable filelink, FSNetworkJSONResponse * _Nonnull response) {
                           // Remove uploaded image from Filestack storage.
                           [uploadedFilelink deleteWithParameters:nil
                                                            queue:dispatch_get_main_queue()
                                                completionHandler:^(FSNetworkDataResponse * _Nonnull response) {
                               // NO-OP;
                           }];

                           if (filelink != nil) {
                               NSURL *documentURL = [self documentURLFor:filelink];

                               [self.transformImageButton setTitle:@"Downloading transformed image..."
                                                          forState:UIControlStateDisabled];

                               // Download transformed image from Filestack storage.
                               [filelink downloadWithDestinationURL:documentURL
                                                         parameters:nil
                                                              queue:dispatch_get_main_queue()
                                                   downloadProgress:nil
                                                  completionHandler:^(FSNetworkDownloadResponse * _Nonnull response) {
                                                      // Remove transformed image from Filestack storage.
                                                      [filelink deleteWithParameters:nil
                                                                               queue:dispatch_get_main_queue()
                                                                   completionHandler:^(FSNetworkDataResponse * _Nonnull response) {
                                                                       // NO-OP;
                                                                   }];

                                                      [self.transformImageButton setEnabled:YES];

                                                      // Update image view's image with our transformed image.
                                                      if (response.destinationURL != nil) {
                                                          self.transformedImageView.image = [UIImage imageWithContentsOfFile:response.destinationURL.path];
                                                      }
                                                  }];
                           } else if (response.error != nil ) {
                               [self.transformImageButton setEnabled:YES];

                               [self presentAlert:@"Transformation Error"
                                          message:error.localizedDescription];
                           }
                       }];
           }
    }];
}

Resources