Hello! I want to show you how to build a relative simple photo sharing app with a twist: your images will be placed on a map background of your current location. You will also be able to add a short message and your name, to end up with an image looking like this (so you can brag to your friends with what you visited in the awesome vacation you have):
To follow this tutorial you need an Apple computer with xCode installed and an Apple developer account.
Table of Contents
ToggleCreate the Project
Open xCode and create a new project named PicTravel. Choose the Single View Application template:
Give the project a name, we chose PicTravel
but you ca choose what other name you like. Also fill in the organization name and the organization identifier for your project:
The last step is to save the newly created project in your Developer
folder.
Building the Interface
From the left panel, open the Main.storyboard
. This is where we will build our app interface.
From the right panel, the bottom part, drag an ImageView and place it in the center of the scene. Resize it so it covers everything (snap it to the blue lines that appear).
Pin the ImageView to the scene margins using the constraints dialog like you see in the image below:
We will use this to put a background image for our app’s main screen. Download the background image from here then open the Assets.xcassets
item from the left panel, click on the “+” button from the bottom and add a new image set named Background
.
Drag the downloaded image to the “1x” placeholder to end up with something like this (we will not use the “2x” and “3x” placeholder for this tutorial, but you can add bigger resolution images to it if you want).
Now, back to the Main.storyboard
, select the ImageView
and, from the Attributes Inspector, select the Background
image and set the Content Mode to Aspect Fill.
It’s time to add a new image to our assets that we will use for the take a photo button we are about to add. Download the image from here, create a new Image Set named Button
in the Assets.xcassets and drag the image into the 1x
placeholder like we did earlier with the background.
Next, in the Main.storybord
, drag a button to the center of the scene. Set it’s Type to Custom
, clear the Title and set the Background to the Button
image we just created. From the Size Inspector, set the button with and height to 115
.
Add two new constraints for the button’s with and heigh (115 each), then, from the Alignment Constriants, align the button horizontally and vertically in the container.
Now let’s add two text fields for the user’s name and message. Place them like in the image below, with the constraints listed below. Don’t forget the blue guides that make the placement easier for you. To create constrains between two elements you can Ctrl+drag from one to another and select the type of constraint you want from the popup that appears.
For the message text field:
- Top Space to the name text field:
15
- Bottom Space to the button:
15
- Width <=
500
- Align Center X to superview
- Trailing Space to superview:
0
with a Priority of900
– that’s very important as it will keep the text field pinned to the margin on iPhones but on bigger devices the width constraint will be stronger so it will not grow larger than 500. - Leading Space to superview:
0
with a Priority of900
For the name field:
- Bottom Space to the message text field:
15
- Width <=
500
- Align Center X to superview
- Trailing Space to superview:
0
with a Priority of900
- Leading Space to superview:
0
with a Priority of900
Set the placeholders for each of the text fields to “Enter Your Name” and “Add a Short Message” so the user knows what to do. Set the Font to Noteworthy Light
with a size of 17
for both.
Also, for the name field set the Capitalization to Words
, Correction and SpellChecking, both to No
. For the message field set the Capitalization to Sentences
.
Here’s the result:
And here’s how it looks on an iPad (you can use the bottom bar under the scene to select on what device you want to preview the interface):
There are two more small elements to add and our main screen will be finished.
Add a label at the bottom with the following text: “Share your travel experiences instantly with your friends & family!”. Set it’s Font to Noteworthly Light 17
, Alignment to center
, Color to #5E7A96
and Lines to 0
. Use the following constrains:
- Trailing Space to superview’s margins:
0
- Leading Space to superview’s margins:
0
- Bottom Space to Bottom Layout:
20
- Height:
64
Finally, add a new view at the top, having a white background with a opacity of 60%. Constrains used are:
- Trailing Space to superview:
0
- Leading Space to superview:
0
- Bottom Space to Top Layout:
20
- Height:
20
Here is the final design:
We will use this screen to get the user’s name and message and open the camera or photo library to take a photo. But after we gel all we need from the user, we need a new screen to show the final image and let him share it. So let’s add a new screen to our app.
From the Object Library, drag a new View Controller near the first one:
Set the background for the view of this new View Controller to #ACE0F6
.
Next, add two buttons like in the image below. Type is custom
, background color is #EE4205
, font Noteworthy Light 17
. Height 40
, width 70
and 110
. They are pinned to the bottom layout + 20
, left or right of the container and also have constraints on with and height.
At the top, add a label to show the current status of the app to the user. Pin it to the top, left and right margins using constrains with a height of 40
. Set the font to Noteworthy Light 19
and color to #EE4205
. The initial text will be “Generating your image…” center aligned.
To show the generated image we need an Image View. Drag it to the center of the second screen and create the following constraints for it:
- Top Space to “Generating your image…” label:
8
- Bottom Space to “Share Image!” button:
8
- Leading Space to superview margin:
0
- Trailing Space to superview margin:
0
Also set it’s Content Mode to Aspect Fit
The last piece of the puzzle is an Activity Indicator View that we will use to visually inform the user that something happens behind the scenes (final image generation) and he has to wait for a few seconds.
Drag it from the Object Library and place it in the middle of the screen. Add constraints to center it horizontally and vertically in the container and set its Behavior to Animating
and Hides When Stopped
.
That’s it, our app’s interface is done. Run the app in the simulator to see how it looks.
Now it’s time to write some code.
Taking a Photo
With the main.storyboard still open, select the button from the first screen then open the Assistant editor and be sure you have the ViewController.swit
file open on the right side. Use the manual mode if you have in case the Assistant editor has a problem figuring this out.
Now, Ctrl+drag from the button to the code to create a new action (be sure to select Action
as the Connection , set the Name to selectPhotoClicked
and the Type to UIButton
):
@IBAction func selectPhotoClicked(_ sender: UIButton) { }
Also Ctrl+drag from the two text fields and the button to the code and create three outlets:
@IBOutlet weak var name: UITextField! @IBOutlet weak var message: UITextField! @IBOutlet weak var selectPhotoButton: UIButton!
Now let’s start with the most interesting part, choosing a photo. The user will be given a choice to take a photo using the camera or to choose one from the photo library. For this we will use an action sheet.
Edit the selectPhotoClicked
method to add the code for this:
@IBAction func selectPhotoClicked(_ sender: UIButton) { let picker = UIImagePickerController() picker.delegate = self let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "Camera", style: .default, handler: {action in picker.sourceType = .camera self.present(picker, animated: true, completion: nil) })) alert.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: { action in picker.sourceType = .photoLibrary self.present(picker, animated: true, completion: nil) })) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) self.present(alert, animated: true, completion: nil) }
To make this code work we need to make our ViewController
a delegate for both UIImagePickerControllerDelegate
and UINavigationControllerDelegate
. To do this change the line defining the ViewController
class to the following:
class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
If you test now the app will crash with an error like “This app has crashed because it attempted to access privacy-sensitive data without a usage description…”. That’s because the app has to ask the user’s permission to access the photo library or the camera and we have to provide a reason for this, reason that will be presented to the user. To do this open the Info.plist
file and add a two new items:
Privacy - Photo Library Usage Description
with a string value ofAccess to the photo library is needed in order to be able to choose a photo to be shared by the app
.Privacy - Camera Usage Description
with a string value ofAccess to the camera is needed in order to be able to take a photo to be shared by the app
.
Also, if you try to choose the “camera” option in the simulator, the app will crash again because the simulator has no camera (“Source type 1 not available”). We need to add a test for this and only present the option if the camera is available:
if UIImagePickerController.isSourceTypeAvailable(.camera) { alert.addAction(UIAlertAction(title: "Camera", style: .default, handler: {action in picker.sourceType = .camera self.present(picker, animated: true, completion: nil) })) }
One more thing to fix before we start handling the actual images: try to run the app on an iPad (in the simulator or on a real device). The app crashes one more time 🙁
Your application has presented a UIAlertController of style UIAlertControllerStyleActionSheet. The modalPresentationStyle of a UIAlertController with this style is UIModalPresentationPopover. You must provide location information for this popover through the alert controller’s popoverPresentationController. You must provide either a sourceView and sourceRect or a barButtonItem. If this information is not known when you present the alert controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation.
That’s because the UIAlertController
on an iPad is presented like a popover (UIModalPresentationPopover
) and it needs a position to be shown at. To do this we will have to test if we have a popoverPresentationController
in the alert
and, if true, set it’s sourceView
and sourceRect
(we are using an Swift optional
for this, as the code after the question mark will be executed only if the popoverPresentationController
is not nil
):
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) // on iPad this is a popover alert.popoverPresentationController?.sourceView = self.view alert.popoverPresentationController?.sourceRect = selectPhotoButton.frame self.present(alert, animated: true, completion: nil)
We also need to make the photo selection from the library to be shown as a popover on an iPad:
alert.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: { action in picker.sourceType = .photoLibrary // on iPad we are required to present this as a popover if UIDevice.current.userInterfaceIdiom == .pad { picker.modalPresentationStyle = .popover picker.popoverPresentationController?.sourceView = self.view picker.popoverPresentationController?.sourceRect = self.selectPhotoButton.frame } self.present(picker, animated: true, completion: nil) }))
Now everything should be fine. Let’s see how to handle the image the user chooses.
We already added a View Controller in the Main.storyboard to show the final image to the user but we need an associated view controller class for it so we have a place to write the code involved in the process.
From the File menu select New -> File… then choose the Cocoa Touch Class option and click Next. Name the new class ShareViewController
and be sure to set it as a subclass of UIViewController
, no XIB
, Swift
language.
Click Next one more time and be sure to have the PicTravel
Group selected and PicTravel
target checked. Then Create the file.
Go back to the Main.storyboard
and select the second view controller and set it’s class to the new ShareViewController
we just created (you can do this from the Identity inspector, the Class dropdown).
Now, with the view controller still selected, open the assistant editor and make sure it is set to automatically open the associated file. This should be the ShareViewController
class. Ctrl+drag from the Image View, Activity Indicator and the Label to the class to create outlets for them:
@IBOutlet weak var imageView: UIImageView! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! @IBOutlet weak var titleLabel: UILabel!
Also add some new variables for the user’s image, name and message:
var image: UIImage! var name: String! var message: String!
So, here’s what we will do next: when the user chooses an image, we instantiate the ShareViewController, set its image
, name
and message
properties to the user’s data then present the ShareViewController
.
To do this we need to create a segue
(transition). Close the assistant editor then, in the Main.storyboard
, Ctrl+drag from the first “View Controller” (click on the yellow symbol from the top-right) to the “Share View Controller” and define a “Present Modally” segue. then click on the created segue and set it’s identifier to showImageSegue
(we will need this later in the code to identify this specific segue).
Now open our ViewController
class. Add a new method to be called when the user selects an image (this is part of the UIImagePickerControllerDelegate
):
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { dismiss(animated: true, completion: nil) image = info[UIImagePickerControllerOriginalImage] as! UIImage performSegue(withIdentifier: "showImageSegue", sender: self) }
Here’s what this method does:
- dismiss the image picker controller
- get the image in a class variable
- present the ShareViewController by performing the
showImageSegue
segue
For this we need a new image variable to be defined in our ViewController
class:
var image: UIImage!
To send the data to the ShareViewController, we will use a method that is called just before the segue to the new view controller happens:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showImageSegue" { if let shareViewController = segue.destination as? ShareViewController { shareViewController.image = self.image shareViewController.name = name.text ?? "" shareViewController.message = message.text ?? "" } } }
Now run the app. After the user selects an image, the second view controller should be presented to the user. But… we have no way to close it. Let’s do this next.
Still in the ViewController
, create an action that will be called when the ShareViewController
will exit (here we will also set the image to nil
to free the memory because we don’t need it anymore):
@IBAction func exit(unwindSegue: UIStoryboardSegue) { image = nil }
Back to the Main.storyboard
, Ctrl+drag from the Close button to the exit symbol from the top-right of the ShareViewController and select the exit method when prompted.
This should solve our problem of closing the second screen.
Let’s get now to the main objective of our app: get the user’s location, generate a map image and put the photo the user selected on top of it together with the name, location and message.
To continue, add some constants to the ShareViewController class that will be used in the code that follows:
// some constants used to generate the final image let finalImageMaxDimension: CGFloat = 2048 let finalImageBorderWidth: CGFloat = 4 let userImageMaxDimension: CGFloat = 1200 let userImageBorderWidth: CGFloat = 20 let userImageX: CGFloat = 100 let userImageY: CGFloat = 160 let mapRegionDistance: CLLocationDistance = 600 let rotateContentByDegrees: CGFloat = -4 let userMessageMaxLength = 100 let textMargin: CGFloat = 280 let userMessageTopMargin: CGFloat = 60 let userNameTopMargin: CGFloat = 80 let userNameHeight: CGFloat = 120
Getting Current User Location
First we need to add a new key to Info.plist
, Privacy - Location When In Use Usage Description
with the value of Current location is needed to generate a map image of your location
.
Now we need to import CoreLocation
into our ShareViewController:
import UIKit import CoreLocation
Declare a new locationManager
variable:
var locationManager:CLLocationManager!
Add a new method that will start the process of finding the user location:
func getCurrentLocation() { locationManager = CLLocationManager() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters locationManager.requestWhenInUseAuthorization() if (CLLocationManager.authorizationStatus() == .denied) { showError(title: "Location Access Denied", message: "The location permission was not authorized. Please enable it in Privacy Settings to allow the app to get your location and generate a map image based on that.") } if CLLocationManager.locationServicesEnabled() { locationManager.startUpdatingLocation() } }
For this to work we need to implement the CLLocationManagerDelegate
protocol:
class ShareViewController: UIViewController, CLLocationManagerDelegate {
We also need to define the showError
function:
func showError(title: String, message: String) { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .default, handler: { _ in self.dismiss(animated: true, completion: nil) }) alert.addAction(action) present(alert, animated: true, completion: nil) }
Now implement the method of the CLLocationManagerDelegate
protocol that is called when we get a location update:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let location = locations[0] manager.stopUpdatingLocation() // get city & country name let geocoder = CLGeocoder() geocoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in if error != nil { self.showError(title: "Whoops...", message: error!.localizedDescription) } else { let placemark = placemarks?[0] self.locationString = (placemark?.administrativeArea ?? "") + ", " + (placemark?.country ?? "") self.generateMapImage(location: location) } }) }
This method will receive the user’s location, stop the location updates (because we don’t need them anymore) and calls a generateMapImage
function (that we’ll define later) with the retrieved location as an argument. It also uses the Apple’s geocoder service to find out the city and country name for the retrieved location and puts them in a new class variable locationString:
var locationString = ""
Generate the Map Image
To generate the map image we will use the following function:
func generateMapImage(location userLocation: CLLocation) { let mapSnapshotOptions = MKMapSnapshotOptions() // Set the region of the map that is rendered. let location = CLLocationCoordinate2DMake(userLocation.coordinate.latitude, userLocation.coordinate.longitude) let region = MKCoordinateRegionMakeWithDistance(location, mapRegionDistance, mapRegionDistance) mapSnapshotOptions.region = region // Set the size of the image output. mapSnapshotOptions.size = calculateMapImageSize(image: self.image) let snapShotter = MKMapSnapshotter(options: mapSnapshotOptions) snapShotter.start(completionHandler: { snapShot, error in if error != nil { self.showError(title: "Whoops1...", message: error!.localizedDescription) } else { self.mapImage = snapShot?.image self.activityIndicator.stopAnimating() self.generateFinalImage() } }) }
You need to import MapKit
into ShareViewController
:
import MapKit
Also a new class variable is used:
var mapImage: UIImage!
To calculate the map size we used another function, calculateMapImageSize
that returns a CGSize
object to be used when generating the map image:
func calculateMapImageSize() -> CGSize { let maxSize = finalImageMaxDimension - 2 * finalImageBorderWidth if image.size.width > image.size.height { return CGSize(width: maxSize, height: round(maxSize * image.size.height / image.size.width)) } else { return CGSize(width: round(maxSize * image.size.width / image.size.height), height: maxSize) } }
Building the Final Image
The last thing we need to do is to generate the final image with all the pieces put together:
func generateFinalImage() { let size = CGSize(width: mapImage.size.width + 2 * finalImageBorderWidth, height: mapImage.size.height + 2 * finalImageBorderWidth) let userImageSize = calculateUserImageFinalSize() // start drawing context UIGraphicsBeginImageContextWithOptions(size, true, 0.0) let context = UIGraphicsGetCurrentContext() // draw the white background let bgRectangle = CGRect(x: 0, y: 0, width: mapImage.size.width + 2 * finalImageBorderWidth, height: mapImage.size.height + 2 * finalImageBorderWidth) context!.saveGState() context!.setFillColor(UIColor.white.cgColor) context!.addRect(bgRectangle) context!.drawPath(using: .fill) context!.restoreGState() // draw the map mapImage.draw(in: CGRect(x: finalImageBorderWidth, y: finalImageBorderWidth, width: mapImage.size.width, height: mapImage.size.height)) // draw a semitransparent white rectage over the map to dim it let transparentRectangle = CGRect(x: finalImageBorderWidth, y: finalImageBorderWidth, width: mapImage.size.width, height: mapImage.size.height) context!.saveGState() context!.setFillColor(UIColor(colorLiteralRed: 255, green: 255, blue: 255, alpha: 0.3).cgColor) context!.addRect(transparentRectangle) context!.drawPath(using: .fill) context!.restoreGState() // rotate the context context!.rotate(by: (rotateContentByDegrees * CGFloat.pi / 180)) // draw white rectangle let rectangle = CGRect(x: userImageX, y: userImageY, width: userImageSize.width + 2 * userImageBorderWidth, height: userImageSize.height + 2 * userImageBorderWidth) context!.saveGState() context!.setFillColor(UIColor.white.cgColor) context!.setShadow(offset: CGSize(width: userImageBorderWidth, height: userImageBorderWidth), blur: 8.0) context!.addRect(rectangle) context!.drawPath(using: .fill) context!.restoreGState() // draw user image image.draw(in: CGRect(x: userImageX + userImageBorderWidth, y: userImageY + userImageBorderWidth, width: userImageSize.width, height: userImageSize.height)) // draw message var truncatedMessage = message if (message.distance(from: message.startIndex, to: message.endIndex) > userMessageMaxLength) { truncatedMessage = message.substring(to: message.index(message.startIndex, offsetBy: userMessageMaxLength)) } let messageFont = UIFont(name: "Noteworthy-Bold", size: 80)! let messageFontAttributes = [ NSFontAttributeName: messageFont, NSForegroundColorAttributeName: UIColor.black, ] as [String : Any] let messageSize = sizeOfString(string: truncatedMessage!, constrainedToWidth: Double(size.width - textMargin), attributes: messageFontAttributes) truncatedMessage!.draw(in: CGRect(x: userImageX + userImageBorderWidth, y: userImageY + userImageBorderWidth + userImageSize.height + userMessageTopMargin, width: size.width - textMargin, height: messageSize.height), withAttributes: messageFontAttributes) // draw name, location & date let nameFont = UIFont(name: "Noteworthy", size: 58)! let nameFontAttributes = [ NSFontAttributeName: nameFont, NSForegroundColorAttributeName: UIColor.black, ] as [String : Any] let dateFormatter = DateFormatter() dateFormatter.dateStyle = .long dateFormatter.timeStyle = .none var nameString = "" if(name != "") { nameString = name + " - " + dateFormatter.string(from: Date()) + ", " + locationString } else { nameString = dateFormatter.string(from: Date()) + ", " + locationString } nameString.draw(in: CGRect(x: userImageX + userImageBorderWidth, y: userImageY + userImageBorderWidth + userImageSize.height + messageSize.height + userNameTopMargin, width: size.width - textMargin, height: userNameHeight), withAttributes: nameFontAttributes) // get final image let finalImage = UIGraphicsGetImageFromCurrentImageContext() // end drawing context UIGraphicsEndImageContext() // show the final image to the user & update tha status label imageView.image = finalImage titleLabel.text = "You can now share your image." }
Here we used two helper methods, calculateUserImageFinalSize
and sizeOfString
. The last one deserves a little attention because it will return a CGSize
object with the dimensions needed to draw a text with specific attributes.
func calculateUserImageFinalSize() -> CGSize { if image.size.width > image.size.height { return CGSize(width: userImageMaxDimension, height: round(userImageMaxDimension * image.size.height / image.size.width)) } else { return CGSize(width: round(userImageMaxDimension * image.size.width / image.size.height), height: userImageMaxDimension) } } func sizeOfString (string: String, constrainedToWidth width: Double, attributes: [String: Any]) -> CGSize { let attString = NSAttributedString(string: string,attributes: attributes) let framesetter = CTFramesetterCreateWithAttributedString(attString) return CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: 0,length: 0), nil, CGSize(width: width, height: .greatestFiniteMagnitude), nil) }
Now that we have everything in place, we need to start the whole process when the ShareViewController is loaded and presented to the user:
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) getCurrentLocation() }
Test the app. Enter your name, a short message and select a photo. You should get the following result:
Sharing the Final Image
Ok, now let’s see how do we share the resulted image. To do this we will use an UIActivityViewController.
Add a new shareImage action for the Share Image! button using Ctrl+drag into the ShareViewController
like we did earlier for the photo button:
@IBAction func shareImage(_ sender: UIButton) { }
The code for the actual sharing is pretty simple:
@IBAction func shareImage(_ sender: UIButton) { let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil) activityViewController.popoverPresentationController?.sourceView = self.view present(activityViewController, animated: true, completion: nil) }
Try it! Now you can save the image or share it with your friends & family using the installed apps on your device.
Final Touches
To finish the app we need to do two more things:
- add an app icon
- make the startup screen look good
To add an app icon, first download the images from here, then open the Assets.xcassets
from the left panel, select the AppIcon item and drag&drop downloaded images to their respective placeholders.
To change the startup screen , which is white right now, open the LaunchScreen.storyboard
from the left panel and drag an Image View to it. Add constraints so it covers the entire screen, select the Background
Image and set the Content Mode to Aspect Fill
(it will look exactly like our main app’s screen background).
That’s it! You can now run the app and even install it on your device to take it out in the wild.
The code for this app is available on github: https://github.com/intelligentbee/PicTravel
You can also download the final app from the App Store: https://itunes.apple.com/us/app/pictravel/id1263495901?mt=8
1 Comment
Comments are closed.
Awesome! Its actually remarkable article, I have
got much clear idea regarding from this article.