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
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.swift
SecondViewController.swift
.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:
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 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 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)
}
}
}
}
}