pod init

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