Installation and usage of the iOS & Swift SDKs

Learn how to pick files, enable the image editor and transform images in iOS.

Overview

In this tutorial we’ll be creating a fresh iOS app where we’ll be able to pick files, edit picked 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 subdependencies.

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.swift
    • SecondViewController.swift.
    • 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.

  8. 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:

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.0'
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 name your file FilestackSetup.swift. Now add the following contents to it making sure to set your API key and app secret accordingly:

import Filestack

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

// Filestack Client, nullable
var fsClient: Filestack.Client?

Open AppDelegate.swift and add the following imports:

import Filestack
import FilestackSDK

Now we are going to add our Filestack client initialization code inside a function called setupFilestackClient():

private func setupFilestackClient() {
    // Create `Policy` object with an expiry time and call permissions.
    let policy = Policy(expiry: .distantFuture,
                        call: [.pick, .read, .stat, .write, .writeURL, .store, .convert, .remove, .exif])

    // Create `Security` object based on our previously created `Policy` object and app secret obtained from
    // https://dev.filestack.com/.
    guard let security = try? Security(policy: policy, appSecret: filestackAppSecret) else {
        fatalError("Unable to instantiate Security object.")
    }

    // Create `Config` object.
    let config = Filestack.Config.builder
        .with(appUrlScheme: appURLScheme)
        .with(imageUrlExportPreset: .current)
        .with(maximumSelectionLimit: 10)
        .with(availableCloudSources: [.dropbox, .googleDrive, .googlePhotos, .customSource])
        .with(availableLocalSources: [.camera, .photoLibrary, .documents])
        .with(documentPickerAllowedUTIs: ["public.item"])
        .build()

    // Instantiate the Filestack `Client` by passing an API key obtained from https://dev.filestack.com/,
    // together with a `Security` and `Config` object.
    // If your account does not have security enabled, then you can omit this parameter or set it to `nil`.
    fsClient = Filestack.Client(apiKey: filestackAPIKey, security: security, config: config)
}

Finally, replace your existing application(_, didFinishLaunchingWithOptions:) with:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    setupFilestackClient()
    return true
}

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 name your file PickerViewController.swift. Now add the following contents to it:

import UIKit
import Filestack
import FilestackSDK

class PickerViewController: UIViewController {

    private var presentPickerButton: UIButton!

    override func viewDidLoad() {
        presentPickerButton = UIButton(type: .system)
        presentPickerButton.setTitle("Present Picker", for: .normal)
        presentPickerButton.addTarget(self, action: #selector(presentPicker(_:)), for: .touchUpInside)
        presentPickerButton.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(presentPickerButton)

        view.addConstraint(NSLayoutConstraint(item:presentPickerButton!,
                                                   attribute: .centerX,
                                                   relatedBy: .equal,
                                                   toItem: view,
                                                   attribute: .centerX,
                                                   multiplier: 1,
                                                   constant: 0))

        view.addConstraint(NSLayoutConstraint(item:presentPickerButton!,
                                              attribute: .centerY,
                                              relatedBy: .equal,
                                              toItem: view,
                                              attribute: .centerY,
                                              multiplier: 1,
                                              constant: 0))
    }

    @IBAction func presentPicker(_ sender: AnyObject) {}
}

Right-click Filestack-Tutorial on the Project Navigator, choose New File… and name your file TransformImagesViewController.swift. Now add the following contents to it:

import UIKit
import Filestack
import FilestackSDK

private struct Images {
    // Original image URL
    static let originalImageURL = Bundle.main.url(forResource: "original", withExtension: "jpg")!
    // Placeholder image URL
    static let placeholderImageURL = Bundle.main.url(forResource: "placeholder", withExtension: "png")!
}

class TransformImagesViewController: UIViewController {
    private let originalImageLabel: UILabel = {
        // Setup original image label
        let label = UILabel()
        label.text = "Original Image"
        label.font = UIFont.preferredFont(forTextStyle: .subheadline)
        label.textColor = .lightGray
        label.setContentCompressionResistancePriority(.required, for: .vertical)

        return label
    }()

    private let originalImageView: UIImageView = {
        // Setup original image view
        let imageView = UIImageView(image: UIImage(contentsOfFile: Images.originalImageURL.path))
        imageView.contentMode = .scaleAspectFit

        NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 220).isActive = true

        return imageView
    }()

    private let transformedImageView: UIImageView = {
        // Setup transformed image view
        let imageView = UIImageView(image: UIImage(contentsOfFile: Images.placeholderImageURL.path))
        imageView.contentMode = .scaleAspectFit

        NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 220).isActive = true

        return imageView
    }()

    private let transformImageButton: UIButton = {
        // Setup transformed image button
        let button = UIButton(type: .system)
        button.setTitle("Transform Image", for: .normal)
        button.addTarget(self, action: #selector(transformImage(_:)), for: .touchUpInside)

        return button
    }()

    private lazy var stackView: UIStackView = {
        // Setup stack view
        let stackView = UIStackView(arrangedSubviews: [
            originalImageLabel,
            originalImageView,
            transformImageButton,
            transformedImageView
        ])

        stackView.axis = .vertical
        stackView.alignment = .center
        stackView.distribution = .fillProportionally
        stackView.spacing = 22
        stackView.translatesAutoresizingMaskIntoConstraints = false

        return stackView
    }()

    override func viewDidLoad() {
        // Add stack view to view hierarchy
        view.addSubview(stackView)
    }

    override func viewDidAppear(_ animated: Bool) {
        // Setup stack view constraints
        let views = ["stackView" : stackView]

        let h = NSLayoutConstraint.constraints(withVisualFormat: "H:|-22-[stackView]-22-|",
                                               metrics: nil,
                                               views: views)

        let w = NSLayoutConstraint.constraints(withVisualFormat: "V:|-top-[stackView]-bottom-|",
                                               metrics: ["top": view.safeAreaInsets.top + 22,
                                                         "bottom": view.safeAreaInsets.bottom + 22],
                                               views: views)

        // Remove existing view constraints
        view.removeConstraints(view.constraints)
        // Add new view constraints
        view.addConstraints(h)
        view.addConstraints(w)

        super.viewDidAppear(animated)
    }

    @IBAction func transformImage(_ sender: AnyObject) {}
}

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 name your file TabBarViewController.swift. Now add the following contents to it:

import UIKit

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        view.backgroundColor = .white

        let pickerViewController = PickerViewController()
        pickerViewController.tabBarItem = UITabBarItem(title: "Image Picker",
                                                       image: UIImage(named: "first"),
                                                       tag: 0)

        let transformImagesViewController = TransformImagesViewController()
        transformImagesViewController.tabBarItem = UITabBarItem(title: "Transform Image",
                                                                image: UIImage(named: "second"),
                                                                tag: 0)

        viewControllers = [pickerViewController, transformImagesViewController]
    }
}

Finally, open AppDelegate.swift and replace application(_, didFinishLaunchingWithOptions:) with:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    setupFilestackClient()

    // Added code — we set our TabBarController as the window's root view controller 
    // and make window key and visible.
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = UINavigationController(rootViewController: TabBarController())
    window?.makeKeyAndVisible()

    return true
}

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 Extensions. Now, right-click Extensions and choose New File… and name it UIViewController+PresentAlert.swift. Add the following contents to it:

import UIKit

extension UIViewController {
    func presentAlert(titled title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))

        self.present(alert, animated: false, completion: nil)
    }
}

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.swift and add the following implementation to presentPicker(_:):

@IBAction func presentPicker(_ sender: AnyObject) {
    guard let fsClient = fsClient else { return }

    // Store options for your uploaded files.
    // Here we are saying our storage location is S3 and access for uploaded files should be public.
    let storeOptions = StorageOptions(location: .s3, access: .public)
    // Instantiate picker by passing the `StorageOptions` object we just set up.
    let picker = fsClient.picker(storeOptions: storeOptions)
    // Set our view controller as the picker's delegate.
    picker.pickerDelegate = self
    // Finally, present the picker on the screen.
    present(picker, animated: true)
}

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:

extension PickerViewController: PickerNavigationControllerDelegate {
    // A file was picked from a cloud source
    func pickerStoredFile(picker: PickerNavigationController, response: StoreResponse) {
        picker.dismiss(animated: false) {
            if let handle = response.contents?["handle"] as? String {
                self.presentAlert(titled: "Success", message: "Finished storing file with handle: \(handle)")
            } else if let error = response.error {
                self.presentAlert(titled: "Error Uploading File", message: error.localizedDescription)
            }
        }
    }

    // A file or set of files were picked from the camera, photo library, or Apple's Document Picker
    func pickerUploadedFiles(picker: PickerNavigationController, responses: [NetworkJSONResponse]) {
        picker.dismiss(animated: false) {
            let handles = responses.compactMap { $0.json?["handle"] as? String }
            let errors = responses.compactMap { $0.error }

            if errors.isEmpty {
                let joinedHandles = handles.joined(separator: ", ")
                self.presentAlert(titled: "Success", message: "Finished uploading files with handles: \(joinedHandles)")
            } else {
                let joinedErrors = errors.map { $0.localizedDescription }.joined(separator: ", ")
                self.presentAlert(titled: "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 AppDelegate.swift, locate the Filestack.Config builder and add the following line to it:

.withEditorEnabled()

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.swift file and let’s first add a helper function:

private func documentURL(for filelink: FileLink) -> URL? {
    guard let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }

    return documentsDirectoryURL.appendingPathComponent(filelink.handle).appendingPathComponent("jpg")
}

And now add the actual implementation for transformImage(_:):

@IBAction func transformImage(_ sender: AnyObject) {
    guard let fsClient = fsClient else { return }

    transformImageButton.isEnabled = false
    transformImageButton.setTitle("Uploading image...", for: .disabled)

    fsClient.upload(from: Images.originalImageURL) { (response) in
        if let error = response?.error {
            self.presentAlert(titled: "Transformation Error", message: error.localizedDescription)
        } else if let json = response?.json, let handle = json["handle"] as? String {
            self.transformImageButton.setTitle("Transforming image...", for: .disabled)

            // Obtain Filelink for uploaded file.
            let uploadedFilelink = fsClient.sdkClient.fileLink(for: handle)
            // Obtain transformable for Filelink.
            let transformable = uploadedFilelink.transformable()
            // Add some transformations.
            transformable.add(transform: ResizeTransform().width(220).height(220).fit(.crop).align(.center))
                .add(transform: RoundedCornersTransform().radius(20).blur(0.25))

            let storageOptions = StorageOptions(location: .s3, access: .public)

            // Store transformed image in Filestack storage.
            transformable.store(using: storageOptions, base64Decode: false) { (filelink, response) in
                // Remove uploaded image from Filestack storage.
                uploadedFilelink.delete(completionHandler: { (response) in
                    print("Removing uploaded image from Filestack storage.")
                })

                if let filelink = filelink {
                    guard let documentURL = self.documentURL(for: filelink) else { return }

                    self.transformImageButton.setTitle("Downloading transformed image...", for: .disabled)

                    // Download transformed image from Filestack storage.
                    filelink.download(destinationURL: documentURL, completionHandler: { (response) in
                        // Remove transformed image from Filestack storage.
                        filelink.delete(completionHandler: { (response) in
                            print("Removing transformed image from Filestack storage.")
                        })

                        self.transformImageButton.isEnabled = true

                        // Update image view's image with our transformed image.
                        if let destinationURL = response.destinationURL {
                            self.transformedImageView.image = UIImage(contentsOfFile: destinationURL.path)
                        }
                    })
                } else if let error = response.error {
                    self.transformImageButton.isEnabled = true
                    self.presentAlert(titled: "Transformation Error", message: error.localizedDescription)
                }
            }
        }
    }
}

Resources