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
Open Xcode and create a new project (File -> New Project), select iOS as the platform and Tabbed App as the template:
On the next step, name your project Filestack-Tutorial, leaving all other settings as default:
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
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: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:
Now go to the Info tab, expand URL Types and click the plus (+) button. Set the identifier to
com.filestack.tutorial
and URL Schemes totutorial
.Add the following keys to Custom iOS Target Properties:
Key | Value |
---|---|
NSPhotoLibraryUsageDescription | This demo needs access to the photo library. |
NSCameraUsageDescription | This demo needs access to the camera. |
Right-click each of the following 2 images and save them locally on your Mac. We will need them in our project:
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:
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 pickerTransformImagesViewController
— 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 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];
}
}];
}
}];
}