iOS Attachments App


Overview

This tutorial enables you to build a fully functioning attachment sharing app in iOS using Swift. The app will allow you to upload photos which sync across devices, showing one of the key features of DittoSwift.

For best results, have WiFi, LAN, or AWDL enabled. Bluetooth is not best for sharing attachments and used solely will limit the performance of this app.


Create Xcode Project

Create a new swift project in Xcode that uses a UICollectionView to display a list of images. We will use Cocoapods to install the SDK.


Setup Cocoapods

For this project we will use Cocoapods to install the SDK.

If you need to install Cocoapods, please follow the installation instructions at Cocoapods.org . CocoaPods version will need to be later than 1.10.0


Integrate DittoSwift

Close your project for now because Cocoapods will create a xcworkspace which automatically integrates the libraries into your project. Then in your terminal navigate to the directory which contains your YourProject.xcodeproj file and run:

   pod init

This will create a Podfile which you will open and add DittoSwift to it:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'YourProject' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for YourProject
  pod 'DittoSyncKitSwift', '~> 1.0.0-alpha7'

end

Save the Podfile and close it then run:

   pod install --repo-update

This will install DittoSwift as a dependency and create a YourProject.xcworkspace file with the SDK integrated. Once it is finished open up YourProject.xcworkspace. You now have Ditto installed in the application! To verify and ensure we can use the SDK add the following line in a UIViewController underneath import UIKit:

   import DittoSyncKitSwift

Permissions

Since iOS 13 and Xcode 11 an app must ask the user's permission to use Bluetooth. DittoKit will activate Bluetooth by default, which means the user will receive a permission prompt automatically. In addition, since iOS 14 an app must ask the user's permission to use the Local Area Network to discover devices.

You must include several keys in the Info.plist file your app

  • Privacy - Local Network Usage Description
  • Privacy - Bluetooth Peripheral Usage Description
  • Privacy - Bluetooth Always Usage Description
  • A Bonjour service _http-alt._tcp.

These can be configured through Xcode's Info project settings.

Alternatively, add the keys directly to Info.plist. Right click on the Info.plist and hover to Open as and then click Source Code

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Uses Bluetooth to connect and sync with nearby devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Uses Bluetooth to connect and sync with nearby devices</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Uses WiFi to connect and sync with nearby devices</string>
<key>NSBonjourServices</key>
<array>
  <string>_http-alt._tcp.</string>
</array>


Create Ditto Constant

Since we’ll be using DittoSwift in multiple classes throughout the app, let’s set up a static instance to be shared. Create a file named Constants.swift and add the following:

import DittoSyncKitSwift

struct Constants {

    static var ditto: DittoSyncKit!

}

Initialize Ditto instance

In your view controller's viewDidLoad method, add the below code to set the properties and start Ditto:

import DittoSyncKitSwift

...

override func viewDidLoad() {
    super.viewDidLoad()
    //UI related code
    
    //Initialize DittoSyncKit
    Constants.ditto = {
        let ditto = DittoSyncKit(identity: .development(appName: "APP NAME"))
        ditto.setAccessLicense("<Access License>")
        ditto.start()
        return ditto
    }()
}

The important things to note is that you need an access license to use Ditto. If you do not have one yet, reach out and we can supply one. To enable background synchronization, we need to call start() which allows you to control when synchronization occurs. For this application we want it to run the entire time the app is in use.


Create Pic Class

In this application we want to be able to insert attachments, as well as observe insertions from other devices. With that, we’ll create a new class called Pic.swift. This class will be a helper for all of our DittoSwift operations with attachments.

import DittoSyncKitSwift
import Foundation
import UIKit

...

struct Pic {
    var id: String
    var imageAttachmentToken: DittoAttachmentToken?
    var mimeType: String

    init (document: DittoDocument) {
        self.id = document.id
        self.imageAttachmentToken = document["image"].attachmentToken
        self.mimeType = document["mimeType"].string ?? "image/jpeg"
    }
}

enum AttachmentUnknownError: Error {
    case message(String)
}

A DittoAttachmentToken serves as a token for a specific attachment that you can use to fetch in the future


Extend UIImage Class

Shortly we'll be writing a UIImage to a file for inserting as an attachment. First, let's extend the UIImage class and create a jpeg function that compresses the image to the quality we pass in

import UIKit

extension UIImage {
    enum JPEGQuality: CGFloat {
            case lowest  = 0
            case low     = 0.25
            case medium  = 0.5
            case high    = 0.75
            case highest = 1
        }

    /// Returns the data for the specified image in JPEG format.
    /// If the image object’s underlying image data has been purged, calling this function forces that data to be reloaded into memory.
    /// - returns: A data object containing the JPEG data, or nil if there was a problem generating the data. This function may return nil if the image has no data or if the underlying CGImageRef contains data in an unsupported bitmap format.
    func jpeg(_ jpegQuality: JPEGQuality) -> Data? {
        return jpegData(compressionQuality: jpegQuality.rawValue)
    }
}

FileManager helper

Create a file called TemporaryFile.swift and place this code in. It will help in creating a temperory directory with unique name.

import Foundation

/// A wrapper around a temporary file in a temporary directory. The directory
/// has been especially created for the file, so it's safe to delete when you're
/// done working with the file.
///
/// Call `deleteDirectory` when you no longer need the file.
struct TemporaryFile {
    let directoryURL: URL
    let fileURL: URL
    /// Deletes the temporary directory and all files in it.
    let deleteDirectory: () throws -> Void

    /// Creates a temporary directory with a unique name and initializes the
    /// receiver with a `fileURL` representing a file named `filename` in that
    /// directory.
    ///
    /// - Note: This doesn't create the file!
    init(creatingTempDirectoryForFilename filename: String) throws {
        let (directory, deleteDirectory) = try FileManager.default
            .urlForUniqueTemporaryDirectory()
        self.directoryURL = directory
        self.fileURL = directory.appendingPathComponent(filename)
        self.deleteDirectory = deleteDirectory
    }
}

extension FileManager {
    /// Creates a temporary directory with a unique name and returns its URL.
    ///
    /// - Returns: A tuple of the directory's URL and a delete function.
    ///   Call the function to delete the directory after you're done with it.
    ///
    /// - Note: You should not rely on the existence of the temporary directory
    ///   after the app is exited.
    func urlForUniqueTemporaryDirectory(preferredName: String? = nil) throws
        -> (url: URL, deleteDirectory: () throws -> Void)
    {
        let basename = preferredName ?? UUID().uuidString

        var counter = 0
        var createdSubdirectory: URL? = nil
        repeat {
            do {
                let subdirName = counter == 0 ? basename : "\(basename)-\(counter)"
                let subdirectory = temporaryDirectory
                    .appendingPathComponent(subdirName, isDirectory: true)
                try createDirectory(at: subdirectory, withIntermediateDirectories: false)
                createdSubdirectory = subdirectory
            } catch CocoaError.fileWriteFileExists {
                // Catch file exists error and try again with another name.
                // Other errors propagate to the caller.
                counter += 1
            }
        } while createdSubdirectory == nil

        let directory = createdSubdirectory!
        let deleteDirectory: () throws -> Void = {
            try self.removeItem(at: directory)
        }
        return (directory, deleteDirectory)
    }
}

Create Helper Functions

Next, back in our Pic.swift file we’ll extend the Pic class and add our first method, allowing us to insert documents consisting of attachments into our pic collection. We pass in a UIImage to the method and create a file with the jpg extenion. Then we insert out attachment along with the file mimeType

extension Pic {
    static func insertPicture(image: UIImage) {
        guard let directory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) as NSURL else { return }
        let fileName = "\(UUID().uuidString).jpg"
        guard let filePath = directory.appendingPathComponent(fileName) else {
            fatalError()
        }
        try? image.jpeg(.low)?.write(to: filePath)
        let attachment = Constants.ditto.store["pics"].newAttachment(path: filePath.path)
        try! Constants.ditto.store["pics"].insert([
            "image": attachment,
            "mimeType": "image/jpeg",
        ])
    }
}

In order to stay up to date with other devices using our attachments app, we need to monitor the collection for any new inserts that may happen. For this, we will add an observer for our pics collection.

extension Pic {

    ....

    static func observePics(_ callback: @escaping (_ pics: [Pic], _ event: DittoLiveQueryEvent) -> Void) -> DittoLiveQuery {
        return Constants.ditto.store["pics"].findAll().observe { (docs, event) in
            let pics = docs.map{ Pic(document: $0) }
            callback(pics, event)
        }
    }
}

Lastly, we need to be able to extract the data from the attachments using our DittoAttachmentToken. Here we fetch the attachment, get the data and write to a file

extension Pic {

    ....

    static func getDataForAttachmentToken(_ token: DittoAttachmentToken, _ callback: @escaping (_ progress: Float?, _ error: Error?, _ image: Data?, _ tempURL: URL?) -> Void) -> DittoAttachmentFetcher? {
        return Constants.ditto.store["pics"].fetchAttachment(token: token) { (event) in
            switch event {
            case .completed(let attachment):
                do {
                    let data = try attachment.getData()
                    let tempURL = try TemporaryFile(creatingTempDirectoryForFilename: "\(UUID().uuidString).jpg").fileURL
                    try data.write(to: tempURL)
                    callback(1, nil, data, tempURL)
                } catch(let error) {
                    callback(nil, error, nil, nil)
                }

                break;
            case .deleted:
                break
            case .progress(let downloadedBytes, let totalBytes):
                callback(Float(downloadedBytes) / Float(totalBytes), nil, nil, nil)
                break
            @unknown default:
                callback(nil, AttachmentUnknownError.message("Unknown error"), nil, nil)
            }
        }
    }
}


Observing Data

Now back in our UIViewController class we can incorporate our methods from the Pic!

Create an array property of our Pic class that will keep hold of all the images in our pics collection

var pics: [Pic] = []

Now create a DittoLiveQuery that we'll be setting in our viewDidLoad to observe changes

var liveQuery: DittoLiveQuery?

To observe images with a DittoLiveQuery, add the following to the bottom of the viewDidLoad method in your ViewController

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    liveQuery = Pic.observePics({ [weak self] (pics, event) in
        guard let `self` = self else { return }
        self.pics = pics
        switch event {
        case .initial:
            self.collectionView.reloadData()
        case .update(let info):
            //Update CollectionView data
            self.collectionView.setData(info)
            break
        @unknown default:
            break
        }
    })
}

Here we are creating an observer for the pics collection, setting our UICollectionView with the initial dataset as well as any updates that may occur.

Adding Images to a Collection

Define a UIImagePickerController class property:

let imagePicker = UIImagePickerController()

Add a simple button that performs an action to select an image from your device’s photo gallery:

@objc func buttonAction() {
    imagePicker.sourceType = .photoLibrary
    imagePicker.allowsEditing = true
    imagePicker.modalPresentationStyle = .fullScreen
    imagePicker.delegate = self;
    present(imagePicker, animated: true, completion: nil)
}

And extend the UIImagePickerController class to handle the selected image and insert using our Pic helper class

extension ViewController : UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            if let data = pickedImage.jpegData(compressionQuality: 0.5) {
                let image = UIImage.init(data: data)!
                Pic.insertPicture(image: image)
            }
            self.dismiss(animated: true, completion: nil)
        }
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }
}

Displaying Images

Finally, in order to visually stay up to date with our pics collection, we can set our UICollectionViewCell with an image from the collection.

First let’s create a custom class for our UICollectionViewCell with a UIImageView property called pictureImageView that equals the bounds of it’s frame and a UIProgressView centered, to show the progress of the image being fetched. The DittoAttachmentFetcher must be kept alive for as long as you wish to observe updates about the associated.

  • attachment.
class PicCollectionViewCell: UICollectionViewCell {
 
    static let REUSE_ID = "PicCollectionViewCell"
    var attachmentFetcher: DittoAttachmentFetcher?
 
    lazy var pictureImageView: UIImageView = {
        let i = UIImageView()
        i.contentMode = .scaleAspectFill
        i.layer.masksToBounds = true
        i.backgroundColor = UIColor.systemGray
        return i
    }()
 
    let progressView: UIProgressView = UIProgressView()
    ...
}   

Now we’ll add a method that will handle the fetching of a Pic image using the unique DittoAttachmentToken. Once the fetching of the data is complete, we can hide our progressView and set our pictureImageView image

class PicCollectionViewCell: UICollectionViewCell {
    ...

    func configureWithPic(_ pic: Pic) {
        guard let attachmentToken = pic.imageAttachmentToken else {
            self.pictureImageView.image = nil
            return
        }
        if pic.mimeType == "image/jpeg" {
        attachmentFetcher = Pic.getDataForAttachmentToken(attachmentToken, { (progress, error, data, _) in
                DispatchQueue.main.async {
                    self.progressView.isHidden = progress == nil || progress == 0 || progress == 1
                    self.progressView.progress = progress ?? 0
                    if let data = data {
                        UIView.transition(with: self.pictureImageView,
                                          duration: 0.3,
                                          options: .transitionCrossDissolve,
                                          animations: {
                                            self.pictureImageView.image = UIImage(data: data)
                                          },
                                          completion: nil)
                    }
                }
            })
        }
    }
}

Now back to our UICollectionView. We can grab the Pic object at the row index and pass it to our configureWithPic method to set the cell’s image

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let pic = pics[indexPath.row]
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PicCollectionViewCell.REUSE_ID, for: indexPath) as! PicCollectionViewCell
    cell.configureWithPic(pic)
    return cell
} 

Build and Run!

🎉 You now have a fully functioning attachments app. Build and run it on the simulator or devices and observe the automatic data sync provided by DittoSyncKit:

iOS Attachments App Syncing
Top