• 10 hours
  • Medium

Free online content available in this course.

Videos available in this course

Certificate of achievement available at the end this course

Got it!

Utilize device resources

Log in or subscribe for free to enjoy all this course has to offer!

We can implement various functionalities within our app. And it doesn’t have to and even should not function in isolation. It must take advantage of resources available outside of the app, such as media elements or contacts.

In this chapter we will learn how to use Camera and Photo Library and will also explore local data storage and user defaults.

Thankfully, we don’t have to reinvent the wheel because  there’s plenty of functionality available for us in Swift frameworks.

Collaborating with other apps

For our convenience, such tools are available for us through Swift libraries. A couple of important aspects are worth mentioning here.

As a general rule of iOS, all apps function in a Sandbox.

Sandbox is a computer security term. In simple terms, it means that each app only has access to dedicated space in any kind of memory. In order for it to access resources outside its Sandbox, it must be done via iOS capabilities as well as with the user's explicit permission.

Obtaining User Permission

You've probably seen an alert popup on your favorite apps asking permission to access your camera, media library, contacts etc.

There are a couple of important requirements enforced by Apple:

  • An app must only request permission for access when it's required for the app's functionality.

  • When asking permission, an app must explicitly communicate what the access is for and how the obtained data is going to be used.

This implies some extra work for us, the developers. But, hey, imagine yourself as a user.  I bet you'd want to know why a stinger-app wants to access your photos. :soleil:

What happens if a user declines?

A user may very well decline access to external resources, in which case they will have to go to the Settings app on their device and make adjustments there.

Our goal is to provide adaptive feedback.  For example, if a user declines our original request, we will notify them on what to do should they change their mind in the future.

Similarly, after accepting the original request for permission, a user may change that setting in the Settings app. Our responsibility is to always verify whether permission is currently  granted. From there, we either proceed with the related functionality or notify the user about why we can't complete a requested action and what they need to do to permit it.

Implementing FrameIT image picking options

Let's remind ourselves of our image picking requirements:

  • Take a photo

  • Library pick

  • Random

We've already created some placeholders for those functions in the previous chapter. They are action handlers for our action sheet alert controller. Let's organize our code a bit further. Instead of dumping all the necessary processing code for each action into a corresponding handler, we are going to create a separate function for each option. And then, call that function within action handlers:

func displayCamera() {
    print("Launching device camera")
}
    
func displayLibrary() {
    print("Launching device library")
}
    
func pickRandom() {
    print("Picking from local images")
}

And then call them with our action handlers:

        // create camera action
        let cameraAction = UIAlertAction(title: "Take photo", style: .default) { (action) in
            self.displayCamera()
        }
        
        // add camera action to alert controller
        alertController.addAction(cameraAction)
        
        // create library action
        let libraryAction = UIAlertAction(title: "Library pick", style: .default) { (action) in
            self.displayLibrary()
        }
        
        // add library action to alert controller
        alertController.addAction(libraryAction)
        
        // create random action
        let randomAction = UIAlertAction(title: "Random", style: .default) { (action) in
            self.pickRandom()
        }
        
        // add random action to alert controller
        alertController.addAction(randomAction)
        
        // create cancel action
        let canceclAction = UIAlertAction(title: "Cancel", style: .cancel)
        
        // add cancel action to alert controller
        alertController.addAction(canceclAction)

Let's review the above, there a couple of things to note:

  • When we call a function with a closure, we are required to explicitly use the self keyword to indicate which function we are referring to.  This is important because, by the time we reach this code to execute, there might be other conflicting functions within that local context.

  • Since we don't need to handle cancelation, we don't have to provide any code in the handler. If so, we can remove the closure altogether. That also makes our code look cleaner!  

The first two functions are going to be similar in structure.

FrameIT + Camera & Library

To begin, we must configure our app to require access to Camera and Library. This parameters are set in, already familiar to us as property lists (our .plist file). Select 'Info.plist' on the Navigator panel. Hover over the last item on the list and click the +  button 2 times. Set the keys and values to the following:

  • Privacy - Camera Usage Description/$(PRODUCT_NAME) camera use

  • Privacy - Photo Library Usage Description/$(PRODUCT_NAME) photo use

As a result, the file should look like this:

Plist configuration for Camera & Library
Plist configuration for Camera & Library
Sneak peek into delegation

Next, our view controller needs to have 2 characteristics, it needs to become a delegate of Navigation Controller and of Image Picker Controller.

What does all this mean? o_O

Our app is going to display some screens we don't have implemented: external resources, another view controller, and Camera/Media Library. It's going to navigate to that new controller. 

So what?

There are different ways to make objects communicate In this case, we're talking about a presented view controller communication back to presenting view controller. One of the ways is to implement that communication back is delegation.

A delegate is a resource outside of an object that is expected to perform some tasks assigned to that object.

For example, when we need to take a photo, we don't need to implement all the associated, sophisticated functionalities, we just need to add a camera. Now that's very handy for a starter indeed!

The camera, in our case,  doesn't care what we do with that photo! Nor does it care when a user changes their mind and cancels a request to take a photo. All the camera wants to do is either take a photo or let us know the user canceled the action.

So, the camera needs a reference to some other object that is able to handle those functions.

That reference must be our view controller!

So, how do we state that our view controller, in fact, is able to perform the tasks that camera will delegate?

To let the camera know our view controller will handle the delegated tasks, we need to declare that our view controller IS a navigation controller delegate (  UINavigationControllerDelegate ) and an Image Picker Delegate (  UIImagePickerControllerDelegate ).

Let me throw another new definition at you. :ange:  Being a delegate means confirming to a protocol.

What's a protocol?

A protocol is a set of properties and methods that an object promises to implement. Some of them are required, some are optional.

Let's make a real life example and make an Apartment object. Let's say it has the task of cleaning. An apartment can't clean itself, it only knows when it's clean! So it creates a mandatory task for a tenant. So it creates a Living protocol (  ApartmentLivingDelegate  ) that has a required method -cleaning.

Now, a tenant, in order to use the apartment, must declare that it is able to perform a cleaning task! Furthermore, it must implement that method; otherwise, it will be kicked out from the residence! Well, it won't be that dramatic, only the app will crash! By the way, if a protocol method is optional instead of required, it won't crash- it just won't get cleaned! Yuck. :'(

Finishing the setup

So, back to the code. To state our view controller conforms to the 2 protocols, we need to list them at the top of declaration where we list the parent class. We also need to import new libraries: Photos , to work with device photos and  AVFoundation , to work with the camera:

import UIKit
import Photos
import AVFoundation

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
...
}

We're going to switch the order of implementation. First, we'll learn how to work with media library, and after that, we'll move to the camera functionality.

Implementing the "Library pick" option

Starting with the displayLibrary() method, here's how to present the library:

let imagePicker =  UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
present(imagePicker, animated: true, completion: nil)

Here's what we've done here:

  • Declared a variable for the image picker controller. This is similar to a custom controller to navigate to or an alert controller we have already worked with.

  • Assigned self (our view controller) as a delegate. This tells the image picker controller that we are ready to handle the associated tasks it needs to delegate.

  • Specified what type of source we'll require: a photo library. Possible types are:

    • camera

    • photoLibrary

    • savedPhotosAlbum

  • Presented an image picker controller

This will often work, but not always. The trouble is, our requested source type may not be available. In the case of a library, if it's empty, it's considered unavailable.

If we leave the code like this, the app will crash. :(

Crashes are a big NO-NO in software development, especially if we are perfectly aware of its potential occurrence. To solve this we must check for the source type availability:

let sourceType = UIImagePickerControllerSourceType.photoLibrary
        
if UIImagePickerController.isSourceTypeAvailable(sourceType) {
    let imagePicker =  UIImagePickerController()
    imagePicker.delegate = self
    imagePicker.sourceType = sourceType
    present(imagePicker, animated: true, completion: nil)
}

Let's see what's new here:

  • We are checking if the source type is available and will proceed with displaying the library only if we have the resource.

  • We are creating a variable to hold the source type we are intending to work with. Then we will use it as we please. We've done this twice in this function, so the utility is already obvious. :zorro:

Well, now the app won't crash. BUT, if the library is not available, it won't display anything and the user will remain puzzled about why they can't pick a photo. We need to let them know.

A simple way to communicate feedback is to display a quick acknowledgement alert stating the fact:

let sourceType = UIImagePickerControllerSourceType.photoLibrary
        
if UIImagePickerController.isSourceTypeAvailable(sourceType) {
    let imagePicker =  UIImagePickerController()
    imagePicker.delegate = self
    imagePicker.sourceType = sourceType
    present(imagePicker, animated: true, completion: nil)
    }
else {
    let alertController = UIAlertController(title: "Oops...", message: "Sincere apologies, it looks like we can't access your photo library at this time.", preferredStyle: .alert)
    let OKAction = UIAlertAction(title: "Got it", style: .cancel)
    alertController.addAction(OKAction)
    present(alertController, animated: true)
}

The else clause is very handy here. When the photo library is not available we are going to show the user an alert. We can't accommodate their request, but at least we can provide some feedback.

 Are we done yet?

One more thing to add before the displaying functionality is done.

What are permissions again?

This is when the user has control over what to give access to and what to keep private

So, let's deal with permissions. Here's the newly imported Photo module come in play. After we check for the  source availability, instead of applying brute force and attempting to display image picker, we do the following permission check:

let status = PHPhotoLibrary.authorizationStatus()
            
switch status {
case .notDetermined:
    PHPhotoLibrary.requestAuthorization({ (newStatus) in
        if newStatus == .authorized {
            print("granted - present image picker!")
        } else {
        print("denied")
        }
    })
    case .authorized:
        print("granted - present image picker!")
    case .denied, .restricted:
        print("denied")
    }

Let's review the above:

  • We accessed the current status.

  • We used switch to handle all possible options of the status value. There are 4:

    • notDetermined - never asked for permission before, need to ask

    • authorized - yay! Asked before and permission was granted 

    • denied and restricted - declined during an ask or was changed in the Settings of the app after

  • And, for simplicity, we've just printed the test functionality (ex.:  print("denied")  ) here instead of actual code.

Before we add useful code to handle each situation, notice that we have 2 types of notes here:

  • "granted - present image picker!"

  • "denied"

Each of them appears twice, which means that if we had useful code there, we'd have to repeat the exact same code twice for each situation. :'(

To optimize this code, we'd need to create 2 functions:

  • presentImagePicker  - to present image picker controller. We already have the code for that. Let's pay attention here that we are using a sourceType variable that will remain in the original function, so we better pass to as a parameter.

  • A function to display a permission alert. Displaying permission alert would be very similar to the alert we are displaying when the source type is not available. The only difference would be the message and we can even keep the same title (Oops...). So we can unify this functionality further and use a single function with message parameter -  troubleAlert  .

Perhaps, you can try to do that on your own:

Done yet?
Done yet?

Here's the code:

func presentImagePicker(sourceType: UIImagePickerControllerSourceType){
    let imagePicker =  UIImagePickerController()
    imagePicker.delegate = self
    imagePicker.sourceType = sourceType
    present(imagePicker, animated: true, completion: nil)
}
    
func troubleAlert(message: String?) {
    let alertController = UIAlertController(title: "Oops...", message: message , preferredStyle: .alert)
    let OKAction = UIAlertAction(title: "Got it", style: .cancel)
    alertController.addAction(OKAction)
    present(alertController, animated: true)
}

Let's review a couple of aspects:

  • in presentImagePicker function we're using a parameter - sourceType to be able to continue using the variable we declared in the original function.

  • in troubleAlert we have message as a parameter, note that it's an optional string. The only place we are using it when creating UIAlertController, which takes an optional message. So, there's no reason to alter this.

Our new functions are ready for action, we just need to insert them into suitable places in the original function we created - displayLibrary:

func displayLibrary() {
        let sourceType = UIImagePickerControllerSourceType.photoLibrary
        
        if UIImagePickerController.isSourceTypeAvailable(sourceType) {
            
            let status = PHPhotoLibrary.authorizationStatus()
            let noPermissionMessage = "Looks like FrameIT have access to your photos:( Please use Setting app on your device to permit FrameIT accessing your library"
            
            switch status {
            case .notDetermined:
                PHPhotoLibrary.requestAuthorization({ (newStatus) in
                    if newStatus == .authorized {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your photo library at this time")
        }
    }

Ah, only 1 unexpected thing here - we have a variable to use as a message for no permission alert -  noPermissionMessage . Otherwise we'd have to repeat the same text twice, and we know it's best to avoid t when possible!

Let's test it! Run the app, click 'Share' button and choose 'Library pick' option. Here's our permission request pops-up!

May I please peek at all your photos?
May I please peek at all your photos?

This message was generated within requestAuthorization method provided by PHPhotoLibrary. The message consists of 2 elements, the second one - "FrameIT photo use" matches the string pattern we provided in plist configuration - "$(PRODUCT_NAME) photo use" and that can be customized as we see fit.

Click 'OK' and here's our album:

Yes!
Yes!

How do we test no permission?

First thing we can test is what if user goes to Settings and denies the access. To simulate that, click on Home button and launch the Settings app. Got to FrameIT settings and select 'Photos', adjust the access permission to 'Never': :'(

Testing with Settings app
Testing with Settings app

Now go back to the app i Simulator - Press Home button and switch to the FrameIT (no need to rerun the app from the Xcode). And here we are, can't access the library anymore:

Access denied:(
Access denied :(

What would've happened if we chose 'Don't Allow' instead? :euh:

To test the original request we must reset this setting for our app. One way to do it is remove the app from simulator. Go to home screen and press and hold on the app icon:

Deleting an app from Simulator
Deleting an app from Simulator

Now try testing from the beginning. This time, when the permission request alert pops up, choose 'Don't Allow' and right after you'll get the very same 'Oops...' message that appeared when we adjusted permission setting using Settings app.

 Well done! :magicien:

Now, how do we hear back from the image picker controller? 

There are 2 possible outcomes after image picker has completed its work:

  • user chose a photo - so we need to get our hands on it.

  • user changed their mind and canceled image picking - oh, well, no much we can do about, but most importantly, we don't even need to do anything.

We assigned our view controller as a delegate to image picker controller, so that when the latter has something worthy of delegating, it will pass it on to us. And, we, in our turn, must be ready - implement appropriate functionality. There are 2 functions to implement respectively:

  • imagePickerController(_ picker: , didFinishPickingMediaWithInfo info: ) - executed after user selected an image.

  • imagePickerControllerDidCancel(_ picker: UIImagePickerController) - executed after user canceled picking image. 

Here's the syntax:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    // got an image!
}
    
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    // canceled
}

Let's focus on getting the image:

First we need to dismiss the controller we presented:

picker.dismiss(animated: true, completion: nil)

 Then we need to take the image that's provided to us in info dictionary -  info[UIImagePickerControllerOriginalImage] . Let's have a variable fetching it for us:

let newImage = info[UIImagePickerControllerOriginalImage] as? UIImage

And, finally, we need to place that image in our frameImageView!

Wait a minute, we'll need to do the same in all 3 cases - also after taking a photo with camera or choosing a random local option. Hence, we gotta have a function for it!

func processPicked(image: UIImage?) {
    if let newImage = image {
        creationImageView.image = newImage
    }
}

Let's review it:

  • We are taking an optional image as parameter. Since image may come from various sources, it's more efficient to validate it in one place - right before applying.

  • We are safely warping it and assigning to image property of our image view. The image view itself would be fine accepting nil. However, from user experience prospective it would be cool to show blank  space. Which in fact would be of Sunshine color - remember, we have the frame view as containing view. :p 

And altogether:

func processPicked(image: UIImage?) {
    if let newImage = image {
        creationImageView.image = newImage
    }
}
    
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    picker.dismiss(animated: true, completion: nil)
    let newImage = info[UIImagePickerControllerOriginalImage] as? UIImage
    func processPicked(image: newImage)
}

And now test! Get to the image picking and select one:

Stunning!
Stunning!

Isn't it gorgeous? :ange:

Implementing "Take photo" option
Strike a pose!
Strike a pose!

Here we come to implement the displayCamera() method:

func displayCamera() {
        let sourceType = UIImagePickerControllerSourceType.camera
        
        if UIImagePickerController.isSourceTypeAvailable(sourceType) {
            
            let status = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)

            let noPermissionMessage = "Looks like FrameIT have access to your camera:( Please use Setting app on your device to permit FrameIT accessing your camera"
            
            switch status {
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted) in
                    if granted {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your camera at this time")
        }
    }

We've got a couple new things here:

  • The source type is now camera.

  • We are using different library to check the access permission status and to request one if needed.

  • And, we slightly altered the messages for alerts.

The rest of functionality is very much the same, that includes access permission status handling, displaying alerts. Even displaying Image picker controller to take a photo.  As a convenient consequence, the same delegate method after taken photo is accepted for use is called!

Analyze these two methods:

  • displayLibrary()

  • displayCamera()

Identify similarities and differences. Transform differences into parameters and use a single function instead of 2 different ones. You may call it  displayMediaPicker(..) .

----

Now, if you have a device handy, let's put the new functionality to the test!

Here's what we've got:

Works like a charm!
Works like a charm!

Is that all? 

This is enough functionality for our purpose. As usual, there's always space for improvement. For instance, we could save a photo to user's device, so that in case the app gets relaunch, a user can reload a previously taken photo that they may not have a chance to retake. That, however, we won't be covering in this course. :p

Implementing "Random" option.

Random option is not related to working with external resources. Instead, we need to do the following:

  • Collect the preloaded images from image assets catalogue into an array.

  • Randomly pick one of the images.

  • Make sure it's different from the current one.

First, let's declare a global array that will hold all the images. It's best to make it global so that we don't have to populate it every time we need to pick a random image, place it right after outlets:

var localImages = [UIImage].init()

Next, we need a function that will collect all available local images that we'll use for random pick:

func collectLocalImageSet() {
    localImages.removeAll()
    let imageNames = ["Boats", "Car", "Crocodile", "Park", "TShirts"]
        
    for name in imageNames {
        if let image = UIImage.init(named: name) {
            localImages.append(image)
        }
    }
}

Let's review the function above:

  • We cleared the localImages array

  • Composed an array of image names, so that we can collect them in a loop

  • Looped through the images names fetching them one by one from the image assets catalogue, and if an a requested image is available, appending it to the array 

Now, we have to call this function when the view controller is loaded. That would be  viewDidLoad  method. However, we'll have more things to implement on the launch and it's better to have a dedicated function for it:

func configure() {
    collectLocalImageSet()
}

 And then, call this new function in viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
        
    configure()
}

The preparation is done!

Next, let's implement a method that would pick a random image. Remember that it must be different from the current one:

func randomImage() -> UIImage? {
        
    let currentImage = creationImageView.image
        
    if localImages.count > 0 {
        while true {
            let randomIndex = Int(arc4random_uniform(UInt32(localImages.count)))
            let newImage = localImages[randomIndex]
            if newImage != currentImage {
                return newImage
            }
        }
    }
    return nil
}

Now let's complete pickRandom() method:

func pickRandom() {

    processPicked(image: randomImage())

}

We're simply calling processPicked method passing a return value from randomImage method.

And here's what we've got so far:

import UIKit
import Photos
import AVFoundation

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
    @IBOutlet weak var creationFrame: UIView!
    @IBOutlet weak var creationImageView: UIImageView!
    
    @IBOutlet weak var colorLabel: UILabel!
    @IBOutlet weak var colorsContainer: UIView!
    
    var creation = Creation.init()
    var localImages = [UIImage].init()
    
    @IBAction func startOver(_ sender: Any) {
        print("Starting over")
    }
    
    @IBAction func applyColor(_ sender: UIButton) {
        print("Applying color")
    }
    
    @IBAction func share(_ sender: Any) {
        //print("Sharing")
        displayImagePickingOptions() // tmp
    }
    
    func displayImagePickingOptions() {
        let alertController = UIAlertController(title: "Choose image", message: nil, preferredStyle: .actionSheet)
        
        // create camera action
        let cameraAction = UIAlertAction(title: "Take photo", style: .default) { (action) in
            self.displayCamera()
        }
        
        // add camera action to alert controller
        alertController.addAction(cameraAction)
        
        // create library action
        let libraryAction = UIAlertAction(title: "Library pick", style: .default) { (action) in
            self.displayLibrary()
        }
        
        // add library action to alert controller
        alertController.addAction(libraryAction)
        
        // create random action
        let randomAction = UIAlertAction(title: "Random", style: .default) { (action) in
            self.pickRandom()
        }
        
        // add random action to alert controller
        alertController.addAction(randomAction)
        
        // create cancel action
        let canceclAction = UIAlertAction(title: "Cancel", style: .cancel)
        
        // add cancel action to alert controller
        alertController.addAction(canceclAction)
        
        present(alertController, animated: true) {
            // code to execute after the controller finished presenting
        }
    }
    
    func displayCamera() {
        let sourceType = UIImagePickerControllerSourceType.camera
        
        if UIImagePickerController.isSourceTypeAvailable(sourceType) {
            
            let status = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)
            
            let noPermissionMessage = "Looks like FrameIT have access to your camera:( Please use Setting app on your device to permit FrameIT accessing your camera"
            
            switch status {
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted) in
                    if granted {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your camera at this time")
        }
    }
    
    func displayLibrary() {
        let sourceType = UIImagePickerControllerSourceType.photoLibrary
        
        if UIImagePickerController.isSourceTypeAvailable(sourceType) {
            
            let status = PHPhotoLibrary.authorizationStatus()
            let noPermissionMessage = "Looks like FrameIT have access to your photos:( Please use Setting app on your device to permit FrameIT accessing your library"
            
            switch status {
            case .notDetermined:
                PHPhotoLibrary.requestAuthorization({ (newStatus) in
                    if newStatus == .authorized {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your photo library at this time")
        }
    }
    
    func presentImagePicker(sourceType: UIImagePickerControllerSourceType) {
        let imagePicker =  UIImagePickerController()
        imagePicker.delegate = self
        imagePicker.sourceType = sourceType
        present(imagePicker, animated: true, completion: nil)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        picker.dismiss(animated: true, completion: nil)
        let newImage = info[UIImagePickerControllerOriginalImage] as? UIImage
        processPicked(image: newImage)
    }
    
    func troubleAlert(message: String?) {
        let alertController = UIAlertController(title: "Oops...", message:message , preferredStyle: .alert)
        let OKAction = UIAlertAction(title: "Got it", style: .cancel)
        alertController.addAction(OKAction)
        present(alertController, animated: true)
    }
    
    func pickRandom() {
        processPicked(image: randomImage())
    }
    
    func randomImage() -> UIImage? {
        
        let currentImage = creationImageView.image
        
        if localImages.count > 0 {
            while true {
                let randomIndex = Int(arc4random_uniform(UInt32(localImages.count)))
                let newImage = localImages[randomIndex]
                if newImage != currentImage {
                    return newImage
                }
            }
        }
        return nil
    }
    
    func collectLocalImageSet() {
        localImages.removeAll()
        let imageNames = ["Boats", "Car", "Crocodile", "Park", "TShirts"]
        
        for name in imageNames {
            if let image = UIImage.init(named: name) {
                localImages.append(image)
            }
        }
    }
    
    func processPicked(image: UIImage?) {
        if let newImage = image {
            creationImageView.image = newImage
        }
    }
    
    func configure() {
        collectLocalImageSet()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        configure()
    }
    
}

This is exciting!

What's next?

Utilizing local storage

There're various types of local storage that are available for iOS apps:

  • User Defaults - accommodates individual data pieces in flat format. 

  • Core Data database - represents relational database.

  • Documents - represent document based form of database.

So far, we've got one piece of data to store that we nee to access at the next relaunch of our app - the latest default color of our frame. It's a single individual piece of data and User Defaults is a perfect form of storage for it!

Utilizing User Defaults

User defaults can be accessed using  UserDefaults  object, let's declare a global variable for it (right here we have our localImages array):

let defaults = UserDefaults.standard

There are two principle methods to unitize it - to set a value and to get a value. And to identify what we are setting and getting we use key - similar to working with dictionaries:

//setting
defaults.setValue(someValue, forKey: someKey)
        
//getting
defaults.value(forKey: someKey)

For example, we can store a string:

let city = "Paris"
defaults.set(city, forKey: "city")

Even though, the value of color variable was a string, all the values get stored as Any?. And, when we retrieve it, we need to convert it to an expected type:

let location = defaults.value(forKey: "city") as? String
print(location) // Paris

Then there are helper methods that simplify working with specific data types, here's a list of functions to retrieve values:

defaults.array(forKey: String)
defaults.bool(forKey: String)
defaults.data(forKey: String)
defaults.dictionary(forKey: String)
defaults.float(forKey: String)
defaults.integer(forKey: String)
defaults.object(forKey: String)
defaults.stringArray(forKey: String)
defaults.string(forKey: String)
defaults.double(forKey: String)
defaults.url(forKey: String)
defaults.value(forKey: String)

We can store various types of data in user defaults, even objects, like UIColor. However, it requires some more knowledge. You'll learn it as you advance your knowledge! :p

Instead, we are going to store an index of selected color. Which would ultimately be of an Int type.

Implementing color change

We can now implement the color change function.

We're going to cheat a little bit here. We'll create an array of colors programmatically and connect it back to the interface (to the view) in our configuration method.

Let's declare the colors array:

var colorSwatches = [ColorSwatch].init()

Then implement collecting colors method:

func collectColors() {
    colorSwatches = [
            ColorSwatch.init(caption: "Ocean", color: UIColor.init(red: 44/255, green: 151/255, blue: 222/255, alpha: 1)),
            ColorSwatch.init(caption: "Shamrock", color: UIColor.init(red: 28/255, green: 188/255, blue: 100/255, alpha: 1)),
            ColorSwatch.init(caption: "Candy", color: UIColor.init(red: 221/255, green: 51/255, blue: 27/255, alpha: 1)),
            ColorSwatch.init(caption: "Violet", color: UIColor.init(red: 136/255, green: 20/255, blue: 221/255, alpha: 1)),
            ColorSwatch.init(caption: "Sunshine", color: UIColor.init(red: 242/255, green: 197/255, blue: 0/255, alpha: 1))
        ]
    
    if colorSwatches.count == colorsContainer.subviews.count {
        for i in 0 ..< colorSwatches.count {
            colorsContainer.subviews[i].backgroundColor = colorSwatches[i].color
        }
    }
}

Let's review the above:

  • We've assigned a new value to colorSwatches array that includes 5 ColorSwatch objects with custom captions and colors.

  • Verified that the number of color swatches matches the number of subviews (buttons) in colorsContainer.

  • (Re)-assigned the background colors to the buttons in order we defined our custom swatches  in colorSwatches property.

 And finally we can complete the user defaults functionality. We'll implement it in a form of a calculated property saving color index in use defaults using 'ColorIndex' key. And since we are going to use the key more than 2 times, it's reasonable to store it in a variable to avoid typos and make it easy to change if needed:

let colorUserDefaultsKey = "ColorIndex"
var savedColorSwatchIndex: Int {
    get {
        let savedIndex = defaults.value(forKey: colorUserDefaultsKey)
        if savedIndex == nil {
            defaults.set(colorSwatches.count - 1, forKey: colorUserDefaultsKey)
        }
        return defaults.integer(forKey: colorUserDefaultsKey)
    }
    set {
        if newValue >= 0 && newValue < colorSwatches.count {
            defaults.set(newValue, forKey: colorUserDefaultsKey)
        }
    }
}

A few more things left to do:

  • Expand functionality of configuration method to set up the frame views. 

  • Incorporate Creation object  since it represents our data model.

  • Save color index on sharing

  • Implement start over functionality (UI StartOver button)

  • Implement apply color functionality (UI color swatch buttons)

Starting with configuration:

func configure() {
    //collect images
    collectLocalImageSet()

    //collect colors
    collectColors()

    //set creation data object
    creation.colorSwatch = colorSwatches[savedColorSwatchIndex]

    // apply creation data to the views
    creationImageView.image = creation.image
    creationFrame.backgroundColor = creation.colorSwatch.color
    colorLabel.text = creation.colorSwatch.caption
}

Let's review the key points:

  •  As the app launches, we are assigning the last saved (or the very default) color swatch to the Creation object.

  • And applying the current state of Creation object to the image view, frame and color label. 

We have to alter our processPicked method to work with Creation object as well: 

func processPicked(image: UIImage?) {
    if let newImage = image {
        creation.image = newImage
        creationImageView.image = creation.image
    }
}

And, finally, we are on a finish line with the last 3 functions! :pirate:

1. Implementing startOver method:

@IBAction func startOver(_ sender: Any) {
     //print("Starting over")
    creation.reset(colorSwatch: colorSwatches[savedColorSwatchIndex])
    creationImageView.image = creation.image
    creationFrame.backgroundColor = creation.colorSwatch.color
    colorLabel.text = creation.colorSwatch.caption
}

This is what we've done:

  • Reset the creation object passing the reset method the latest saved color swatch.

  • And applied the new, reset, creation settings to the view: image, image frame and color label.

2. Implementing applyColor method:

@IBAction func applyColor(_ sender: UIButton) {
    //print("Applying color")
    if let index = colorsContainer.subviews.index(of: sender) {
        creation.colorSwatch = colorSwatches[index]
        creationFrame.backgroundColor = creation.colorSwatch.color
        colorLabel.text = creation.colorSwatch.caption
    }
}

Let's review:

  • We are making sure we can identify index of a pressed button matching the subviews of the color swatches container. 

  • Assigning new color swatch to Creation object.

  • Applying new color swatch to the view - image frame and color label (but not the image!).

3. Altering share method:

@IBAction func share(_ sender: Any) {
    //print("Sharing")
    if let index = colorSwatches.index(where: {$0.caption == creation.colorSwatch.caption}) {
        savedColorSwatchIndex = index
    }
    displayImagePickingOptions() // tmp
}

Here's what we've added:

  • Getting index of the color swatch based on the current swatch of Creation object. 

  • And assigning that index to the savedColorSwatchIndex calculated property. That, in turn, takes care of saving it to User Defaults.

And here's the view controller code altogether:

import UIKit
import Photos
import AVFoundation

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

    @IBOutlet weak var creationFrame: UIView!
    @IBOutlet weak var creationImageView: UIImageView!

    @IBOutlet weak var colorLabel: UILabel!
    @IBOutlet weak var colorsContainer: UIView!

    var creation = Creation.init()
    var localImages = [UIImage].init()
    let defaults = UserDefaults.standard
    var colorSwatches = [ColorSwatch].init()

    let colorUserDefaultsKey = "ColorIndex"
    var savedColorSwatchIndex: Int {
        get {
            let savedIndex = defaults.value(forKey: colorUserDefaultsKey)
            if savedIndex == nil {
                defaults.set(colorSwatches.count - 1, forKey: colorUserDefaultsKey)
            }
            return defaults.integer(forKey: colorUserDefaultsKey)
        }
        set {
            if newValue >= 0 && newValue < colorSwatches.count {
                defaults.set(newValue, forKey: colorUserDefaultsKey)
            }
        }
    }

    @IBAction func startOver(_ sender: Any) {
        //print("Starting over")
        creation.reset(colorSwatch: colorSwatches[savedColorSwatchIndex])
        creationImageView.image = creation.image
        creationFrame.backgroundColor = creation.colorSwatch.color
        colorLabel.text = creation.colorSwatch.caption
    }

    @IBAction func applyColor(_ sender: UIButton) {
        //print("Applying color")
        if let index = colorsContainer.subviews.index(of: sender) {
            creation.colorSwatch = colorSwatches[index]
            creationFrame.backgroundColor = creation.colorSwatch.color
            colorLabel.text = creation.colorSwatch.caption
        }
    }

    @IBAction func share(_ sender: Any) {
        //print("Sharing")
        if let index = colorSwatches.index(where: {$0.caption == creation.colorSwatch.caption}) {
            savedColorSwatchIndex = index
        }
        displayImagePickingOptions() // tmp
    }

    func displayImagePickingOptions() {
        let alertController = UIAlertController(title: "Choose image", message: nil, preferredStyle: .actionSheet)

        // create camera action
        let cameraAction = UIAlertAction(title: "Take photo", style: .default) { (action) in
            self.displayCamera()
        }

        // add camera action to alert controller
        alertController.addAction(cameraAction)

        // create library action
        let libraryAction = UIAlertAction(title: "Library pick", style: .default) { (action) in
            self.displayLibrary()
        }

        // add library action to alert controller
        alertController.addAction(libraryAction)

        // create random action
        let randomAction = UIAlertAction(title: "Random", style: .default) { (action) in
            self.pickRandom()
        }

        // add random action to alert controller
        alertController.addAction(randomAction)

        // create cancel action
        let canceclAction = UIAlertAction(title: "Cancel", style: .cancel)

        // add cancel action to alert controller
        alertController.addAction(canceclAction)

        present(alertController, animated: true) {
            // code to execute after the controller finished presenting
        }
    }

    func displayCamera() {
        let sourceType = UIImagePickerControllerSourceType.camera

        if UIImagePickerController.isSourceTypeAvailable(sourceType) {

            let status = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)

            let noPermissionMessage = "Looks like FrameIT have access to your camera:( Please use Setting app on your device to permit FrameIT accessing your camera"

            switch status {
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted) in
                    if granted {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your camera at this time")
        }
    }

    func displayLibrary() {
        let sourceType = UIImagePickerControllerSourceType.photoLibrary

        if UIImagePickerController.isSourceTypeAvailable(sourceType) {

            let status = PHPhotoLibrary.authorizationStatus()
            let noPermissionMessage = "Looks like FrameIT have access to your photos:( Please use Setting app on your device to permit FrameIT accessing your library"

            switch status {
            case .notDetermined:
                PHPhotoLibrary.requestAuthorization({ (newStatus) in
                    if newStatus == .authorized {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your photo library at this time")
        }
    }

    func presentImagePicker(sourceType: UIImagePickerControllerSourceType) {
        let imagePicker =  UIImagePickerController()
        imagePicker.delegate = self
        imagePicker.sourceType = sourceType
        present(imagePicker, animated: true, completion: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        picker.dismiss(animated: true, completion: nil)
        let newImage = info[UIImagePickerControllerOriginalImage] as? UIImage
        processPicked(image: newImage)
    }

    func troubleAlert(message: String?) {
        let alertController = UIAlertController(title: "Oops...", message:message , preferredStyle: .alert)
        let OKAction = UIAlertAction(title: "Got it", style: .cancel)
        alertController.addAction(OKAction)
        present(alertController, animated: true)
    }

    func pickRandom() {
        processPicked(image: randomImage())
    }

    func randomImage() -> UIImage? {

        let currentImage = creation.image

        if localImages.count > 0 {
            while true {
                let randomIndex = Int(arc4random_uniform(UInt32(localImages.count)))
                let newImage = localImages[randomIndex]
                if newImage != currentImage {
                    return newImage
                }
            }
        }
        return nil
    }

    func collectLocalImageSet() {
        localImages.removeAll()
        let imageNames = ["Boats", "Car", "Crocodile", "Park", "TShirts"]

        for name in imageNames {
            if let image = UIImage.init(named: name) {
                localImages.append(image)
            }
        }
    }

    func collectColors() {
        colorSwatches = [
            ColorSwatch.init(caption: "Ocean", color: UIColor.init(red: 44/255, green: 151/255, blue: 222/255, alpha: 1)),
            ColorSwatch.init(caption: "Shamrock", color: UIColor.init(red: 28/255, green: 188/255, blue: 100/255, alpha: 1)),
            ColorSwatch.init(caption: "Candy", color: UIColor.init(red: 221/255, green: 51/255, blue: 27/255, alpha: 1)),
            ColorSwatch.init(caption: "Violet", color: UIColor.init(red: 136/255, green: 20/255, blue: 221/255, alpha: 1)),
            ColorSwatch.init(caption: "Sunshine", color: UIColor.init(red: 242/255, green: 197/255, blue: 0/255, alpha: 1))
        ]
        if colorSwatches.count == colorsContainer.subviews.count {
            for i in 0 ..< colorSwatches.count {
                colorsContainer.subviews[i].backgroundColor = colorSwatches[i].color
            }
        }
    }

    func processPicked(image: UIImage?) {
        if let newImage = image {
            creation.image = newImage
            creationImageView.image = creation.image
        }
    }

    func configure() {
        //collect images
        collectLocalImageSet()

        //collect colors
        collectColors()

        //set creation data object
        creation.colorSwatch = colorSwatches[savedColorSwatchIndex]

        // apply creation data to the views
        creationImageView.image = creation.image
        creationFrame.backgroundColor = creation.colorSwatch.color
        colorLabel.text = creation.colorSwatch.caption
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        configure()
    }

}

Let's test it! Run the app, load a photo (or not :)), and click on color swatches, click 'Share' and close the app, then reopen and observe the saved settings apply. Then click on color swatches again and hit 'Start over' button - observe the frame go back to the latest saved state at the last sharing! :magicien:

We're finally done for this chapter! Nice work! 

Let's Recap!

  • To access device resources outside the app, like Camera, Media Library, Contacts our app must request user's permission.

  • Permission settings can be changed by the user at any time, therefore the app must verify current permission status each time before using related resources. 

  • To use a particular external resource in the app, we must configure plist file indicating the intension.

  • User Defaults are one of the forms of storing data on a device that can be retrieved after the app's relaunch.

Example of certificate of achievement
Example of certificate of achievement