SnapShot: How to Create a Digital Scrapbook in iOS - Part 2

Caroline Baillie

Welcome to Part 2 of SnapShot: A Digital iOS Scrapbook! If you haven’t seen Part 1, I recommend you check it out as it takes care of all of the setup and registration.

Just to refresh your mind, SnapShot is an iOS app made using Xcode and SashiDo. It is kind of like a digital scrapbook, but for more information, here is the video demo:

Again, I recommend looking at SnapShot Part 1, or even my first tutorial Fish Classification iOS App with SashiDo and Teachable Machine). Additionally, all my code is available on my GitHub.

Table of Contents

Map

Adding the map is probably the biggest step in this project. Below I have outlined all the steps I took to create and connect everything. However, if you want more visual instructions on how to create and display the map, I recommend checking out this video playlist!

Setup

     1. Go to the storyboard map controller (I created mine on the default ViewController)
     2. Add MKMapView controller
     3. Drag the sides so the MKMapView takes up the whole screen
     4. On the top bar, click Editor -> Resolve Auto Layout Issues -> Add Missing Constraints (selected views)
mapConstraints
     5. Open dual editors (storyboard and ViewController.swift)
         a. Control drag from MKMapView into the ViewController class
         b. Call the outlet mapView
     6. Select the MKMapView/mapView
     7. Control drag to ViewController and click delegate
     8. Make sure you have import MapKit and import CoreLocation in ViewController.swift
     9. Make sure ViewController.swift conforms to MKMapViewDelegate
We also have to set up a reload function that could be called from a different controller (used when memory is edited). The code is also given in the Extras Backpropagation Function Section, but basically there are only two sections of code for ViewController.swift:
     1. In viewDidLoad add this:

// setup for function that is called from newMemory
NotificationCenter.default.addObserver(self, selector: #selector(reload), name: Notification.Name("reload"), object: nil)

     2. Add the following reload function inside the class:

// function to reload newMemories bc when dismissed adding controller doesn't show without
@objc func reload (notification: NSNotification){
    self.mapView.removeAnnotations(self.mapView.annotations)
    for m in sessionManager.shared.memories {
        // if there is a mapView (protection)
        if mapView != nil {
            // add that Memory
            mapView.addAnnotation(m)
        }
    }
}

Memory Class

     1. Inside the Classes folder, create a new Cocoa Touch Class file
     2. Make sure your options are filled in as follows:
classMemory
     3. Click next and create
     4. Inside this file, create a class with all the elements of each memory:

import UIKit
import MapKit

class Memory: NSObject, MKAnnotation {
    var title: String?
    var coordinate: CLLocationCoordinate2D
    var desc: String
    var category: String?
    var tags: String?
    var location: String
    var date: String
    var Image: UIImage
    var objID: String
    
    init(title:String, Image:UIImage, coordinate:CLLocationCoordinate2D,desc:String,category:String,tags:String, location:String,date:String, objID:String){
        self.title = title
        self.Image = Image
        self.coordinate = coordinate
        self.desc = desc
        self.category = category
        self.tags = tags
        self.location = location
        self.date = date
        self.objID = objID
    }
}

Now you can create a variable of type Memory and add it to the annotation map. However, we want to use the information from the database to populate this annotation. Therefore we must create a function in sessionManager.

Request Memories

For our current task, we want a function that gets all the data from SashiDo:
     1. Inside the sessionManager class, add var memories : [Memory]! = [] to create an array of type Memory
     2. Add the following function:

// get all memories
func GetAllMem(completion:@escaping (_ success:Bool) -> ()){
    // clear previous data
    memories.removeAll()
    // get all rows
    let query = PFQuery(className: "Memory")
    // where user is equal to current user
    query.whereKey("userID", equalTo:user.id)
    query.findObjectsInBackground { (objects, error) in
        // no errors
        if error == nil {
            // if there are objects in the array
            if let returnedObjects = objects {
                // loop through all objects in array
                for object in returnedObjects {
                    // extract the image
                    let file = object["Image"] as! PFFileObject
                    file.getDataInBackground { (imageData: Data?, error: Error?) in
                        if error == nil {
                            if let imageData = imageData {
                                let image = UIImage(data: imageData)
                                // convert coord of type GeoPoint to CLLocationCoordinate2D
                                let coor = object["coordinate"]! as! PFGeoPoint
                                let coord = CLLocationCoordinate2D(latitude: coor.latitude, longitude: coor.longitude)
                                // create a new memory
                                let memData = Memory(title: object["title"]! as! String, Image: (image ?? UIImage(named: "test"))!, coordinate: coord, desc: object["desc"]! as! String, category: object["category"]! as! String, tags: object["tags"]! as! String, location: object["location"]! as! String, date: object["date"]! as! String, objID: object.objectId!)
                                // add memory to the global array
                                self.memories.append(memData)
                            }
                        }
                    }
                }
            }
            completion(true)
        }
        else {
            //return false completion if fails
            completion(false)
        }
    }
}

         a. Essentially the function is getting all the information from the database rows that correspond with the current user (using Parse Documentation)
         b. It then loops through all the rows and creates a variable of type Memory
         c. Finally, it adds that memory to the array
     3. Now go to settings.swift and add this code to viewDidLoad:

// get all memories from database
sessionManager.shared.GetAllMem { (success) in
        if success {
            self.removeSpinner()
        }
}

         a. This means that whenever a user logs in, all their information will be saved to the array memories in sessionManager (sessionManager.shared.memories)
     4. To show these memories as annotations on the map, return to ViewContoller.swift and add this code in viewDidLoad:

// go through all memories in global list
for m in sessionManager.shared.memories {
    // if there is a mapView (protection)
    if mapView != nil {
        // add that Memory
        mapView.addAnnotation(m)
    }
}

         a. This code just loops through all the memories and adds the memory to the mapView

Interactive Annotations

Now that we have displayed all our memories, we want to be able to click on them and open a new controller that displays all the information. To do this we need to add two functions to ViewController.swift:
     1. This function changes the style of the pin and displays the i button when clicked:

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    // make sure only right type of pins display
    guard annotation is Memory else {return nil}
    // create identifier for annotation views
    let identifier = "Memory"
    // get back an annotation if it is one with identifier, otherwise nil
    var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
    if annotationView == nil {
        annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
        annotationView?.canShowCallout = true
        //create button - i button for info
        let btn = UIButton(type: .detailDisclosure)
        annotationView?.rightCalloutAccessoryView = btn
    } else {
        // if have in dq go ahead and use
        annotationView?.annotation = annotation
    }
    return annotationView
}

     2. This function presents a different view controller when the i button is clicked

func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
    // make sure is type Memory
    guard let memory = view.annotation as? Memory else {return}
    if control == view.rightCalloutAccessoryView {
        // go to info page for that memory
        let obj = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "memInfoPage") as! memInfoPage
        // pass memory information
        obj.page = memory
        self.present(obj, animated: true, completion: nil)
   }
}

Memory Info Page

The previous code showed you how to make an annotation clickable and open a new controller. It also sent information to the next controller (memInfoPage). To display this information in memInfoPage.swift you must:
     1. Create outlets for all the storyboard elements (labels, images, etc.)
     2. Add var page : Memory! to the top of the class to collect the memory passed from ViewContoller.swift
     3. In viewDidLoad, pass the information from page into all the storyboard elements
     4. Add a function to reload the controller (this is needed for editing the memory)
     5. All together it should look like this:

import Foundation
import UIKit
import Parse

class memInfoPage : UIViewController {
    // var from prev page
    var page : Memory!
    // connections
    @IBOutlet weak var memTitle: UILabel!
    @IBOutlet weak var memImage: UIImageView!
    @IBOutlet weak var memDesc: UITextView!
    @IBOutlet weak var memCategory: UILabel!
    @IBOutlet weak var memLocation: UILabel!
    @IBOutlet weak var memTags: UITextView!
    @IBOutlet weak var memDate: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.memTitle.text = page.title
        memTitle.adjustsFontSizeToFitWidth = true
        self.memImage.image = page.Image
        self.memDesc.text = page.desc
        self.memCategory.text = page.category
        self.memLocation.text = page.location
        self.memTags.text = page.tags
        self.memDate.text = page.date
        NotificationCenter.default.addObserver(self, selector: #selector(reloadContent), name: Notification.Name("reloadContent"), object: nil)
    }
    
    @objc func reloadContent (notification: NSNotification){
        self.memTitle.text = page.title
        memTitle.adjustsFontSizeToFitWidth = true
        self.memImage.image = page.Image
        self.memDesc.text = page.desc
        self.memCategory.text = page.category
        self.memLocation.text = page.location
        self.memTags.text = page.tags
        self.memDate.text = page.date
    }
}

Now when you click the i button on an annotation, a page with all the information about the memory will appear.

Add Annotation

The next logical step is to create a way to add new memories:
     1. Add a button to the storyboard map controller and create an action function (addAnnotation) connecting to ViewController.swift
     2. Insert the following code to present a camera controller (picController):

@IBAction func addAnnotation(_ sender: Any) {
    // should pop up different controller so that can enter info
    let obj = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "picController") as! picController
    self.present(obj, animated: true, completion: nil)
}

To create picController.swift:
     1. If you haven’t already, create a controller with a button and imageView
     2. Create a file called picController.swift with the following code:

import Foundation
import UIKit

class picController: UIViewController {
    
    @IBOutlet weak var img: UIImageView!

     // create camera
    let pickerController = UIImagePickerController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // set camera
        pickerController.sourceType = UIImagePickerController.SourceType.camera
        pickerController.delegate = self
    }
    // linked to button that shows the camera
    @IBAction func onClickTakePic(_ sender: Any) {
        present(pickerController, animated: true, completion: nil)
    }
}
// image taken
extension picController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true, completion: nil)
        if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            // set image taken to image on screen
            img.image = image
            dismiss(animated: true, completion: nil)
            // send image to create new mem
            let obj = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "newMemory") as! newMemory
            obj.image = image
            self.present(obj, animated: true, completion: nil)
        }
    }
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        //close out (camera has been closed)
    }
}

     3. Connect img and onClickTakePic to the image and button on the controller
     4. Allow camera access:
         a. Go to the outermost snapShot.xcodeproj
         b. info -> Custom iOS Target Properties -> right click → add row -> Privacy - Camera Usage Description
privacyProperties
From the code, you can see that once the picture is taken, it sends the information to a different controller called newMemory.

New Memory

newMemory is the controller where the user inputs all the different information on the memory they wish to create. The corresponding file has 4 major parts to it:
     1. Basic setup
     2. Gets users location
     3. Gets current date
     4. Creates a new memory
Breaking each part down…

Basic setup:
     1. Add var image : UIImage! to the top of the newMemory class (it will store the image passed from picController)
     2. Create connections between each input field, image, and button
     3. In viewDidLoad, add self.memImage.image = image to set the image

Location:
     1. Allow location access:
         a. Go to the outermost snapShot.xcodeproj
         b. info -> Custom iOS Target Properties -> right click add row (x2) -> Privacy - Location When In Use Usage Description & Privacy - Location Always and When In Use Usage Description
     2. Add private var locationManager:CLLocationManager? to the top of the newMemory class so we can access the user’s location
     3. Add the following two functions to the class:

func getUserLocation() {
    locationManager = CLLocationManager()
    locationManager?.requestAlwaysAuthorization()
    locationManager?.startUpdatingLocation()
    locationManager?.delegate = self
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    if let location = locations.last {
        sessionManager.shared.currentCoord = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
    }
}

     4. In sessionManager.swift add var currentCoord : CLLocationCoordinate2D? as a variable at the top of the class (this will store the users current coordinate)
     5. Finally, back in newMemory.swift, create a variable for the location in the save button function (saveToggled)

getUserLocation()
let coord = sessionManager.shared.currentCoord

Date:
     1. Add the following to saveToggled:

let date = Date()
let calendar = Calendar.current
let y = calendar.component(.year, from: date)
let m = calendar.component(.month, from: date)
let d = calendar.component(.day, from: date)
let dateNow = "\(m)/\(d)/\(y)"

New Memory:
     1. In saveToggled, create a new variable of type Memory and save it to sessionManager

Code all together:

import Foundation
import UIKit
import MapKit
import Parse
import CoreLocation

class newMemory : UIViewController, UITextFieldDelegate, CLLocationManagerDelegate {
    
    var image : UIImage!
    private var locationManager:CLLocationManager?
    
    @IBOutlet weak var memTitle: UITextField!
    @IBOutlet weak var memCategory: UITextField!
    @IBOutlet weak var memImage: UIImageView!
    @IBOutlet weak var memDesc: UITextView!
    @IBOutlet weak var memLocation: UITextField!
    @IBOutlet weak var memTags: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Set image from passed in value
        self.memImage.image = image
    }
    
    //dismiss keyboard when tapped
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
    
    @IBAction func saveToggled(_ sender: Any) {
        self.showSpinner()
        // find current location
        getUserLocation()
        let coord = sessionManager.shared.currentCoord
        // get current date
        let date = Date()
        let calendar = Calendar.current
        let y = calendar.component(.year, from: date)
        let m = calendar.component(.month, from: date)
        let d = calendar.component(.day, from: date)
        let dateNow = "\(m)/\(d)/\(y)"
        // create new memory
        let newMem = Memory(title: memTitle.text!, Image: image, coordinate: coord, desc: memDesc.text!, category: memCategory.text!, tags: memTags.text!, location: memLocation.text!, date: dateNow, objID: "none")
        // save memory
        sessionManager.shared.saveMemory(memory: newMem) { (success) in
            // reload map controller
            NotificationCenter.default.post(name: Notification.Name("reload"), object: nil)
            // move to next view controller
            self.dismiss(animated: true, completion: nil)
        }
    }
    // get user location stuff
    func getUserLocation() {
        locationManager = CLLocationManager()
        locationManager?.requestAlwaysAuthorization()
        locationManager?.startUpdatingLocation()
        locationManager?.delegate = self
    }
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            sessionManager.shared.currentCoord = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
        }
    }
}

In sessionManager.swift, the function to save a memory looked like this:
     1. Set all the database rows to be the inputted information
     2. Reduce the image size (otherwise might cause error)
     3. Save changes to the database
     4. Add the new memory to global array (memories)

func saveMemory (memory:Memory, completion:@escaping (_ success:Bool) -> ()) {
    // set values for new memory
    let mem = PFObject(className:"Memory")
    mem["title"] = memory.title
    mem["desc"] = memory.desc
    mem["location"] = memory.location
    mem["date"] = memory.date
    mem["userID"] = user.id
    mem["tags"] = memory.tags
    mem["category"] = memory.category
    mem["coordinate"] = PFGeoPoint(latitude: memory.coordinate.latitude, longitude: memory.coordinate.longitude)
    // reducing image size
    let image = memory.Image
    let actualHeight:CGFloat = image.size.height
    let actualWidth:CGFloat = image.size.width
    let imgRatio:CGFloat = actualWidth/actualHeight
    let maxWidth:CGFloat = 1024.0
    let resizedHeight:CGFloat = maxWidth/imgRatio
    let compressionQuality:CGFloat = 0.5
    let rect:CGRect = CGRect(x: 0, y: 0, width: maxWidth, height: resizedHeight)
    UIGraphicsBeginImageContext(rect.size)
    image.draw(in: rect)
    let img: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
    let imageData:Data = img.jpegData(compressionQuality: compressionQuality)!
    UIGraphicsEndImageContext()
    let imageFinal = UIImage(data: imageData)!
    // prepping to save image
    let imgData = imageFinal.pngData()
    let imageFile = PFFileObject(name:"image.png", data:imgData!)
    mem["Image"] = imageFile
    // save all
    mem.saveInBackground { (succeeded, error)  in
        if (succeeded) {
           // The object has been saved.
            memory.objID = mem.objectId as! String
            self.memories.append(memory)
            completion(true)
        } else {
            // There was a problem
            completion(false)
        }
    }
}

Delete

I wanted to allow the user to delete a memory if they no longer wanted it. Fortunately, this process was relatively simple:
     1. Create an action function from the memInfoPage delete button (deleteToggled)
     2. Create an alert asking the user to confirm that they want to delete this item
     3. If canceled dismiss
     4. If confirmed call the sessionManager delete function and return to the dismiss the controller

@IBAction func deleteToggle(_ sender: Any) {
    // alert are you sure you want to delete this
    let confirmAlert = UIAlertController(title: "Delete", message: "Are you sure you want to delete this memory?", preferredStyle: UIAlertController.Style.alert)
    // alert confirmed
    confirmAlert.addAction(UIAlertAction(title: "Confirm", style: .default, handler: { (action: UIAlertAction!) in
        // run delete function
        sessionManager.shared.deleteMemory(memory: self.page) { (success) in
            // reload map controller
            NotificationCenter.default.post(name: Notification.Name("reload"), object: nil)
            self.dismiss(animated: true, completion: nil)
        }
    }))
    // alert canceled
    confirmAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (action: UIAlertAction!) in
        // do nothing
    }))
    // show the alert
    present(confirmAlert, animated: true, completion: nil)
}

To delete the memory in the sessionManager.swift function I had to:
     1. Find that memory in the database
     2. Delete it from the database
     3. Delete it from the global memories array (by comparing objectIDs)

func deleteMemory (memory:Memory, completion:@escaping (_ success:Bool) -> ()) {
    // get the memory
    let query = PFQuery(className:"Memory")
    query.getObjectInBackground(withId: memory.objID) { (object, error) in
        if error == nil {
            // Success!
            if let object = object {
                // delete this row
                object.deleteInBackground()
                // delete from array memories
                self.deleteMemArray(memory: memory)
            }
            completion(true)
        } else {
            // Fail!
            completion(false)
        }
    }
}

func deleteMemArray(memory:Memory) {
    // loop through all memories in array
    for i in 0..<memories.count-1 {
        // if the objectIDs match
        if memories[i].objID == memory.objID {
            // delete the memory
            memories.remove(at: i)
        }
    }
}

I used two separate functions as a stylistic choice.

Edit

I also wanted users to be able to edit memories instead of having to delete and recreate them. This proved to be a little bit harder, but I eventually got it to work:
     1. Create an action function from the memInfoPage edit button (editToggled)
     2. Present and pass information to the edit controller (editMem)

@IBAction func editToggle(_ sender: Any) {
    // put all inputs into the text stuff to be resaved
    let VC = self.storyboard?.instantiateViewController(withIdentifier: "editMem") as! editMem
    // pass memory information
    VC.page = page
    self.present(VC, animated: true, completion: nil)
}

     3. In editMem.swift, write the following code:

import Foundation
import UIKit
import MapKit
import Parse
import CoreLocation

class editMem: UIViewController, UITextFieldDelegate, CLLocationManagerDelegate {
    
    var page : Memory!
    
    @IBOutlet weak var memTitle: UITextField!
    @IBOutlet weak var memCategory: UITextField!
    @IBOutlet weak var memImage: UIImageView!
    @IBOutlet weak var memDesc: UITextView!
    @IBOutlet weak var memTags: UITextView!
    @IBOutlet weak var memLocation: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // set all input fields to the previous values
        memTitle.text = page.title
        memCategory.text = page.category
        memDesc.text = page.desc
        memTags.text = page.tags
        memLocation.text = page.location
        memImage.image = page.Image
    }
    
    @IBAction func memUpdate(_ sender: Any) {
        self.showSpinner()
        // can't edit coord or image
        let coord = page.coordinate
        let image = page.Image
        // create new memory with inputted info
        let newMem = Memory(title: memTitle.text!, Image: image, coordinate: coord, desc: memDesc.text!, category: memCategory.text!, tags: memTags.text!, location: memLocation.text!, date: page.date, objID: page.objID)
        // call update function
        sessionManager.shared.updateMemory(memory: newMem) { (success) in
            self.removeSpinner()
            // go back to memInfoPage
            let VC = self.storyboard?.instantiateViewController(withIdentifier: "memInfoPage") as! memInfoPage
            VC.page = newMem
            self.dismiss(animated: true, completion: nil)
            // call reload functions so update appears
            NotificationCenter.default.post(name: Notification.Name("reloadContent"), object: nil)
            NotificationCenter.default.post(name: Notification.Name("reload"), object: nil)
        }
    }
}

         a. This code first sets all the inputs equal to whatever the previous information on the memory was
         b. The user can then edit the information and click the update button when finished (which runs memUpdate)
         c. memUpdate creates a new memory with the updated information and runs the updateMemory function in sessionManager
         d. It then dismisses the controller (returning to memInfopage)
         e. Finally, it triggers the reload functions mentioned earlier in memInfoPage and ViewController so that the updated information is displayed
In sessionManager, create an update function that:
     1. Finds the object in the database
     2. Replaces with and saves the new information
     3. Replaces the old memory with the new memory in the global memories array (by comparing objectIDs)

func updateMemory (memory:Memory, completion:@escaping (_ success:Bool) -> ()) {
    // find memory
    let query = PFQuery(className:"Memory")
    // with the same objectID
    query.getObjectInBackground(withId: memory.objID) { (object, error) in
        if error == nil {
            // Success!
            if let object = object {
                // update all values
                object["title"] = memory.title
                object["desc"] = memory.desc
                object["location"] = memory.location
                object["date"] = memory.date
                object["tags"] = memory.tags
                object["category"] = memory.category
            }
            // save object
            object!.saveInBackground()
            // change in global array
            self.updateMemArray(memory: memory)
            completion(true)
        } else {
            // Fail!
            completion(false)
        }
    }
}

func updateMemArray(memory:Memory) {
    // loop through memories
    for m in memories {
        // if objectIDs the same
        if m.objID == memory.objID {
            // update that memory in the global array
            m.title = memory.title
            m.desc = memory.desc
            m.location = memory.location
            m.date = memory.date
            m.tags = memory.tags
            m.category = memory.category
        }
    }
}

Again, using two functions is stylistic.

Storing

Most of the previous code has been edited to exclude the sorting tableViewControllers and collectionViewControllers. This is because accessing information this way presented challenges when editing the memories (it was difficult to get the updated information to show up when clicking the navigation back button). Although I managed to do it in the end, including those steps and code in this tutorial added another layer of length and complexity. However, if you are interested in adding a similar kind of sorting mechanism, check out my code on GitHub to see how it was done!

Closing Remarks

Congratulations, you have now created your own digital scrapbook! I know I learned a lot from this project, and hope you did, too. I encourage all readers to embark on the rewarding journey of experimenting with and creating tutorials about Xcode, SashiDo, and iOS Maps. There are so many great resources all over the internet that help make your life easier when coding so there really is no downside to taking things into your own hands and having fun. I hope you all enjoyed my tutorial and again, make sure to check out (or download) my code on GitHub to get the full experience or my previous tutorial that uses Teachable Machine to classify fish types.

Thanks so much to SashiDo and all you readers! Happy future coding!

Resources

SnapShot: How to Create a Digital Scrapbook in iOS - Part 1
An Xcode Collection of Useful Functions and Tips
Video Demo
SnapShot GitHub Repo
SashiDo
Parse Documentation
Parse Video Playlist

Caroline Baillie

A high school junior with a strong passion for computer science and engineering. Currently interning with SashiDo and creating tutorials using their incredible platform!

Find answers to all your questions

Our Frequently Asked Questions section is here to help.