#swift-photofeed
In this tutorial, we are going to make a photo feed app, using Skygear as the backend.
Before getting started, here are the prerequisites:
- Xcode 8 or above
- Swift 3
- Cocoapods (You can refer to the guide)
- A Skygear account (you can sign up here)
After signing up/logging in to the Skygear portal, you can see the screen below.
To create our photo feed app, just click on the + Create New App box. You will be directed to the app creation page. Name the app {your_name}photofeed, in this case I will use the name "vita", so the app name is vitaphotofeed.
Click on Create App when you are done. You will be directed to the app dashboard. Follow the path Getting Started > iOS > New App, you will see the setup guide as shown below.
For easier setup, we will show the steps here. Since we had Cocoapods installed, we will start by scaffolding the app. Open the Terminal on your Mac, navigate to the desired folder, and run the scaffolding command:
pod lib create --silent --template-url=https://github.com/SkygearIO/skygear-Scaffolding-iOS.git "vitaphotofeed"
In your case, you will replace "vitaphotofeed" with your "{your_name}photofeed".
After finishing scaffolding, we will be prompted with few questions to setup the project:
What is your name?
> <your_git_username>
What is your email?
> <your_git_email>
What is your skygear endpoint (You can find it in portal)?
Example: https://myapp.skygeario.com
> https://vitaphotofeed.skygeario.com
What is your skygear API key (You can find it in portal)?
Example: dc0903fa85924776baa77df813901efc
> <your-api-key>
What language do you want to use?? [ Swift / ObjC ]
> Swift
For name and email, enter the username and email your use for Git. The skygear endpoint and skygear API key information is located in the portal with the path Info > Server Detail. Just copy and paste the values to answer the questions. For language, type in Swift.
Upon answering all the questions, scaffolding will begin. After that, Xcode will open with the project Skygear scaffolded for you. You will be prompted whether to convert the syntax to the lastest Swift. Click on Convert.
Next, choose Convert to Swift 3, then click Next.
Tick all the targets, then click Next.
Finally, click Save.
You are not done yet, there is still one last step before the project is properly setup. In the Project Navigator of Xcode, open the file ViewController.swift, as in the picture below:
Then, convert all the NSLog() to print() to make it a Swift syntax.
In ViewController.swift, you have the code as below:
@IBAction func didTapLogin(_ sender: AnyObject) {
SKYContainer.default().login(withUsername: usernameField.text, password: passwordField.text) { (user, error) in
if (error != nil) {
self.showAlert(error as! NSError)
return
}
NSLog("Logged in as: %@", user)
self.updateLoginStatus()
}
}
@IBAction func didTapSignup(_ sender: AnyObject) {
SKYContainer.default().signup(withUsername: usernameField.text, password: passwordField.text) { (user, error) in
if (error != nil) {
self.showAlert(error as! NSError)
return
}
NSLog("Signed up as: %@", user)
self.updateLoginStatus()
}
}
@IBAction func didTapLogout(_ sender: AnyObject) {
SKYContainer.default().logout { (user, error) in
if (error != nil) {
self.showAlert(error as! NSError)
return
}
NSLog("Logged out")
self.updateLoginStatus()
}
}
Replace all the lines with NSLog() with print(), as in below:
@IBAction func didTapLogin(_ sender: AnyObject) {
SKYContainer.default().login(withUsername: usernameField.text, password: passwordField.text) { (user, error) in
if (error != nil) {
self.showAlert(error as! NSError)
return
}
print("Logged in as \(user)") // Here
self.updateLoginStatus()
}
}
@IBAction func didTapSignup(_ sender: AnyObject) {
SKYContainer.default().signup(withUsername: usernameField.text, password: passwordField.text) { (user, error) in
if (error != nil) {
self.showAlert(error as! NSError)
return
}
print("Signed up as \(user)") // And here
self.updateLoginStatus()
}
}
@IBAction func didTapLogout(_ sender: AnyObject) {
SKYContainer.default().logout { (user, error) in
if (error != nil) {
self.showAlert(error as! NSError)
return
}
print("Logged out") // And finally here
self.updateLoginStatus()
}
}
Now, all the code in the project is of the latest Swift syntax. Before finishing the setup, there is one last thing to do: the SkyKit installed by Cocoapods is v0.13.0, which is outdated. To correct these, we are going to install the latest SkyKit (v0.19.0) as of the writing of this article.
To do that, open Finder or Terminal and navigate to the project directory. You will see a Podfile in this directory.
Open the Podfile with your favorite text editor, and change the version of SkyKit from v0.13.0 to v0.19.0, as shown below:
use_frameworks!
target 'vitaphotofeed' do
pod 'SKYKit', '~> 0.19.0' #Changed from 0.13.0
end
Now, we need to install the latest pod by running the following command using Terminal on the Podfile directory:
pod install
Finally, we're done setting up the project. You can now re-open the project by double clicking on {your_name}photofeed.xcworkspace using Finder.
The picture above show the storyboard of the photo feed app. We will have a UINavigationController as the main navigator of the app, and a Login UIViewController as the root controller of the navigation controller. Once the user has logged in, he/she can proceed to the Home UITableViewController.
In Xcode, open Main.storyboard in the project navigator. By default, the Login UIViewController has already been scaffolded for you. What you need to do are:
- Drag and drop a UINavigationController into the storyboard.
- Detach the connected UITableViewController from the UINavigationController.
- Make the UINavigationController the initial view controller of the app.
- Make the Login UIViewController the root view controller of the UINavigationController.
- Change the UINavigationBar title of the Login UIViewController to "Login". Drag and drop a Bar Button Item onto the top right of the UINavigationBar and name it "Proceed".
- Change the UINavigationBar title of the Home UITableViewController to "Home". Drag and drop a Bar Button Item onto the top right of the UINavigationBar and make it a system icon Add.
- Connect the Proceed Button of the Login UIViewController to the detached Home UITableViewController with a default show segue. (By selecting the Proceed Button and Ctrl + Drag to the Home UITableViewController)
There you go, the overall layout is done!
Most of the logic for the Login UIViewController has already been scaffolded for you in the corresponding file ViewController.swift. What we need to implement is to show/hide the Proceed button after users signed up, logged in, and logged out of the app.
To do this, we will open the side-by-side view. First, open Main.storyboard, and select Login UIViewController. Then, click on the Assistant Editor (the one on top right corner of Xcode, with two circles tangled together). Now you will have a side-by-side view of the storyboard layout of Login UIViewController and the logic file ViewController.swift.
Press Ctrl and click on the Proceed Button at the same time, then drag it to ViewController.swift right below the line:
@IBOutlet weak var loginStatusLabel: UILabel!
Enter the name "proceedButton" in the name field of the pop out box, then click Connect. You've successfully connected Proceed Button to the logic file.
We need to implement the show/hide logic of the Proceed Button whenver the login status is updated; therefore, we will write it in the following function:
func updateLoginStatus() {
if ((SKYContainer.default().currentUserRecordID) != nil) {
loginStatusLabel.text = "Logged in"
loginButton.isEnabled = false
signupButton.isEnabled = false
logoutButton.isEnabled = true
proceedButton.title = "Proceed" // 1
proceedButton.isEnabled = true // 2
} else {
loginStatusLabel.text = "Not logged in"
loginButton.isEnabled = true
signupButton.isEnabled = true
logoutButton.isEnabled = false
proceedButton.title = "" // 3
proceedButton.isEnabled = false // 4
}
}
Line numbered 1, 2, 3, 4 are what we need to add to the existing function updateLoginStatus(). Line 1 and 2 are used to show the Proceed Button when users are logged in, while Line 3 and 4 are used to hide the Proceed Button when users are not logged in / logged out.
Click and run the app on a simulator. Sign up for an account, or log in if you've already created one. Then, tap on the Proceed Button.
You will notice that there is a "Login" word at the top left corner of the Back Button as in the left picture above. To remove the "Login" word, we will add the following lines in ViewController.swift, right below the override func viewDidLoad():
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Remove text from back button on next controller
let backBarButtonItem = UIBarButtonItem()
backBarButtonItem.title = ""
navigationItem.backBarButtonItem = backBarButtonItem
}
Run the app on simulator again. Now you will have a clean back button.
A photo is some kind of data we retrieve from the internet. We will retrieve a list of photos from the internet and show them in our app, so we need a structure to store these photos. To do that, we will create a class for photo. First, create a new file named "Photo.swift":
import UIKit
class Photo {
var recordName: String
var imageUrl: URL?
var likes: Int = 0
init(recordName: String, imageUrl: URL) {
self.recordName = recordName
self.imageUrl = imageUrl
}
}
Every Photo will have a unique identifier, which is the recordName. It also contains a URL for the its image, and the number of likes of for it. The "init(recordName: String)" function is used to initialise a Photo. To make parsing the number of likes for string display easier, we will add a helper variable like this:
import UIKit
class Photo {
var recordName: String
var imageUrl: URL?
var likes: Int = 0
var likesToString: String {
get {
if likes == 0 {
return "No like yet"
} else if likes == 1 {
return "\(likes) Like"
} else {
return "\(likes) Likes"
}
}
}
init(recordName: String, imageUrl: URL) {
self.recordName = recordName
self.imageUrl = imageUrl
}
}
The "likesToString: String" variable will parse a gramatically correct string whenever it is called. This make our code more tidy.
There are few actions we will use Skygear for:
- Upload photo and create a record
- Delete photo record
- Add one like to the double-tapped photo
- Retrieve all photos to show on Home
To do that, create a new file named "PhotoHelper.swift". We will create a Helper class to help us manage these complex operations, so life will be easier. Notice that we also import SKYKit below import UIKit in this class file:
import UIKit
import SKYKit
class PhotoHelper {
}
Imagine one user uploading a photo that is 4K to our server. This is going to take an unreasonable amount of time for uploading, retrieving, and taking up too many space to store. Therefore, before we upload the photo, we have to resize it to a proper size. To do that, we will write a resize function in PhotoHelper:
import UIKit
import SKYKit
class PhotoHelper {
static func resize(image: UIImage, maxWidth: CGFloat, quality: CGFloat = 1.0) -> Data? {
var actualWidth = image.size.width
var actualHeight = image.size.height
let heightRatio = actualHeight / actualWidth
print("FROM: \(actualWidth)x\(actualHeight) ratio \(heightRatio)")
if actualWidth > maxWidth {
actualWidth = maxWidth
actualHeight = maxWidth * heightRatio
}
print("TO: \(actualWidth)x\(actualHeight)")
let rect = CGRect(x: 0, y: 0, width: actualWidth, height: actualHeight)
UIGraphicsBeginImageContext(rect.size)
image.draw(in: rect)
guard let img = UIGraphicsGetImageFromCurrentImageContext(),
let imageData = UIImageJPEGRepresentation(img, quality) else {
return nil
}
return imageData
}
}
The "static func resize(…)" function above takes a UIImage and resize it according to specified maximum width and quality. Notice that it is a static function, meaning that we can use this function just by calling PhotoHelper.resize(…) from anywhere. This function will also return an image data of type Data for easier uploading to Skygear server.
That is it. Now we can write the functions for the 4 actions for Skygear: upload, delete, add one like, retrieve.
Quick Notes:
- We will be using the Public DB of Skygear, so that all users can access to all the photos posted
- We are using onCompletion handler in the 4 functions below. This is because it takes time for network requests to finish, especially when uploading photos. These 4 functions basically wait for network requests to finish only then return a value. To know more about onCompletion handler, you can read this.
- In Skygear, a SKYAsset and SKYRecord are stored differently. Hence, we upload a photo as SKYAsset, then only we create a new SKYRecord to link to the SKYAsset.
import UIKit
import SKYKit
class PhotoHelper {
static let container = SKYContainer.default()!
static let publicDB = SKYContainer.default().publicCloudDatabase!
// Action 1: Upload photo and create a record
static func upload(imageData: Data, onCompletion: @escaping (_ succeeded: Bool) -> Void) {
guard let asset = SKYAsset(data: imageData) else {
onCompletion(false)
return
}
asset.mimeType = "image/jpg"
container.uploadAsset(asset, completionHandler: { uploadedAsset, error in
if let error = error {
print("Error uploading asset: \(error)")
onCompletion(false)
} else {
if let uploadedAsset = uploadedAsset {
print("Asset uploaded: \(uploadedAsset)")
let photo = SKYRecord(recordType: "photo")
photo?.setObject(0, forKey: "likes" as NSCopying)
photo?.setObject(uploadedAsset, forKey: "asset" as NSCopying)
publicDB.save(photo!, completion: { record, error in
if let error = error {
// Error saving
print("Error saving record: \(error)")
onCompletion(false)
} else {
if let recordID = record?.recordID {
print("Saved recor with RecordID: \(recordID)")
onCompletion(true)
}
}
})
} else {
onCompletion(false)
}
}
})
}
// Action 2: Delete photo record
static func delete(photo: Photo, onCompletion: @escaping (_ succeeded: Bool) -> Void) {
guard let record = SKYRecord(recordType: "photo", name: photo.recordName) else {
onCompletion(false)
return
}
publicDB.deleteRecord(with: record.recordID, completionHandler: { deletedRecord, error in
if let error = error {
print("Error deleting record: \(error)")
onCompletion(false)
} else {
onCompletion(true)
}
})
}
// Action 3: Add one like to photo record
static func addOneLike(to photo: Photo, onCompletion: @escaping (_ result: SKYRecord?) -> Void) {
guard let record = SKYRecord(recordType: "photo", name: photo.recordName) else {
onCompletion(nil)
return
}
let newLikes = photo.likes + 1
record.setObject(newLikes, forKey: "likes" as NSCopying!)
publicDB.save(record, completion: { savedRecord, error in
if let error = error {
print("Error adding like: \(error)")
onCompletion(nil)
} else {
onCompletion(savedRecord)
}
})
}
// Action 4: Retrieve all photo records
static func retrieveAll(onCompletion: @escaping (_ result: [Photo]) -> Void) {
let query = SKYQuery(recordType: "photo", predicate: NSPredicate(format: "likes >= 0"))
let sortDescriptor = NSSortDescriptor(key: "_created_at", ascending: false)
query?.sortDescriptors = [sortDescriptor]
var photos = [Photo]()
publicDB.perform(query!, completionHandler: { assets, error in
if let error = error {
print("Error retrieving photos: \(error)")
onCompletion(photos)
} else {
guard let assets = assets else {
onCompletion(photos)
return
}
for asset in assets {
guard let record = asset as? SKYRecord,
let likes = record.object(forKey: "likes") as? Int,
let imageAsset = record.object(forKey: "asset") as? SKYAsset else {
continue
}
let photo = Photo(recordName: record.recordID.recordName, imageUrl: imageAsset.url)
photo.likes = likes
photos.append(photo)
}
onCompletion(photos)
}
})
}
static func resize(image: UIImage, maxWidth: CGFloat, quality: CGFloat = 1.0) -> Data? {
var actualWidth = image.size.width
var actualHeight = image.size.height
let heightRatio = actualHeight / actualWidth
print("FROM: \(actualWidth)x\(actualHeight) ratio \(heightRatio)")
if actualWidth > maxWidth {
actualWidth = maxWidth
actualHeight = maxWidth * heightRatio
}
print("TO: \(actualWidth)x\(actualHeight)")
let rect = CGRect(x: 0, y: 0, width: actualWidth, height: actualHeight)
UIGraphicsBeginImageContext(rect.size)
image.draw(in: rect)
guard let img = UIGraphicsGetImageFromCurrentImageContext(),
let imageData = UIImageJPEGRepresentation(img, quality) else {
return nil
}
return imageData
}
}
Before mingling with the storyboard, let's first import the required icons into Xcode. You can download the required icons here. Once downloaded, open Images.scassets in Xcode, then add Love and Placeholder image assets. Finally, drag and drop the downloaded icons into each of the image asset according to their sizes.
Now, it's time to make the layout for our photo feeds. First, open Main.storyboard. Then, lay out the design by:
- Drag the height of the UITableViewCell to appropriate height. We have a square UIImageView, 8px from top, and a UILabel 12px below the UIImageView, of height 21px, and 20px to the bottom of the UITableViewCell. So the appropriate height for an iPhone 7 (screen width 375px) = 8px + 375px + 12px + 21px + 20px = 436px
- Drag and drop a UIImageView into the UITableViewCell. Add constraints so that the UIImageView is 8px from top, 0px to both left and right, and with an aspect ratio of 1:1. For attributes, set the image of the UIImageView as the "Placeholder" image asset we imported just now. Next, tick on the Clip To Bounds option and choose the content mode as Aspect Fill.
- Drag and drop a UILabel below the UIImageView. Add constraints so that the UILabel is 12px below the UIImageView, 16px to both left and right, and 20px above the bottom of the UITableViewCell. Next, make the text align right, set the font weight to Medium and font size to 13, and the content of the text as "--".
- Drag another UIImageView into the UITableViewCell. Add constraints so that the UIImageView is 125px width by 125px height, centered both vertically and horizontally in the Placeholder UIImageView. Next, set the image of the UIImageView as the "Love" image asset, and choose the content mode as Aspect Fit.
We have laid out the UI for the Photo Table View Cell. Now we need to set up the logic of it. First, create a new file named "PhotoTableViewCell.swift". In the file, declare the class for the Photo Table View Cell as followed:
import UIKit
class PhotoTableViewCell: UITableViewCell {
}
Now, we have to connect the Photo Table View Cell on the storyboard to the one in this "PhotoTableViewCell.swift" file. Therefore, in Main.storyboard, click on the Photo Table View Cell, in its Identity inspector select its class as PhotoTableViewCell. After that, you can open the Assistant editor (side-by-side pane), if "PhotoTableViewCell.swift" is not shown automatically on the right pane, you can select it manually. You need to drag and drop each elements of the into the "PhotoTableViewCell.swift" as followed:
import UIKit
class PhotoTableViewCell: UITableViewCell {
@IBOutlet weak var photoView: UIImageView! // For the photo image view
@IBOutlet weak var loveView: UIImageView! // For the love icon image view
@IBOutlet weak var likesLabel: UILabel! // For the number of likes label
}
Now, we set up a double tap gesture recognizer to the Photo Table View Cell to handle the like action from user:
- Hide the Love icon when the Photo Table View Cell is first loaded.
- Add a Double tap gesture recognizer to the content view of the Photo Table View Cell
- Add a "doubleTapped(…)" function to handle the double tap action from user, which is to animate the Love icon and send the PhotoHelper.addOneLike(…) request to the server.
import UIKit
class PhotoTableViewCell: UITableViewCell {
var photo: Photo?
@IBOutlet weak var photoView: UIImageView!
@IBOutlet weak var loveView: UIImageView!
@IBOutlet weak var likesLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
loveView.isHidden = true
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(doubleTapped(sender:)))
doubleTap.numberOfTapsRequired = 2
contentView.addGestureRecognizer(doubleTap)
}
func doubleTapped(sender: UITapGestureRecognizer) {
guard let photo = photo else {
return
}
PhotoHelper.addOneLike(to: photo, onCompletion: { result in
if let likes = result?.object(forKey: "likes") as? Int {
photo.likes = likes
self.likesLabel.text = photo.likesToString
}
})
loveView.isHidden = false
loveView.alpha = 0
UIView.animate(withDuration: 0.25, animations: {
self.loveView.alpha = 1
}, completion: { finished in
UIView.animate(withDuration: 0.25, delay: 0.1, options: .curveEaseInOut, animations: {
self.loveView.alpha = 0
}, completion: { finished in
self.loveView.isHidden = true
})
})
}
}
We have done most of the behind-the-scene logic from the app. Now, it's time to work on the main interface of the app: the Home Controller.
iOS is a very secure system. It will protect user data and we can't just simply get the photos from our users' iPhone without their permission. Hence, let's add a privacy setting in the SupportingFile > Info.plist to seek permission from the user the let us access their photo library.
In Info.plist, add Privacy - Photo Library Usage Description - We need to access your photo library. You can use other string value for the reason field here.
To work on the Home Controller, create a new file named "HomeController.swift":
import UIKit
class HomeController: UITableViewController {
}
Open Main.storyboard, click on the Home Table View Controller, and
- Set its class as HomeController
- Drag and drop Plus bar button item to HomeController.swift as an IBAction, and name it "uploadButtonTapped".
import UIKit
class HomeController: UITableViewController {
@IBAction func uploadButtonTapped(_ sender: Any) {
}
}
Now, we will handle image selection and image upload when the user taps on the Plus bar button item. To do that, let's create an HomeController extension:
-
Create a photos variable of type [Photo] to store all the photos retrieved from server
-
Create the reloadPhotos() function to retrieve all photos and refresh the table once finished
-
Create the function presentImagePicker() to present a photo gallery for user to pick an image from
-
Implement the delegate method ..didFinishPickingMediaWithInfo… to decide what to do after user finishing picking an image
-
Add the function presentImagePicker() back into the uploadButtonTapped(_ sender: Any) function.
import UIKit
class HomeController: UITableViewController {
var photos = [Photo]()
func reloadPhotos() {
PhotoHelper.retrieveAll(onCompletion: { result in
self.photos = result
self.tableView.reloadData()
})
}
@IBAction func uploadButtonTapped(_ sender: Any) {
presentImagePicker()
}
}
extension HomeController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func presentImagePicker() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.modalPresentationStyle = .popover
imagePicker.allowsEditing = false
imagePicker.sourceType = .photoLibrary
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage,
let resizedImageData = PhotoHelper.resize(image: pickedImage, maxWidth: 800, quality: 0.9) {
PhotoHelper.upload(imageData: resizedImageData, onCompletion: { succeeded in
if succeeded {
print("Upload succeeded")
self.reloadPhotos()
} else {
print("Upload failed")
}
})
}
dismiss(animated: true, completion: {
})
}
}
Run the app in simulator. You should now be able to upload photo to the server.
Why aren't the photos shown after we've retrieved them from server? It's because we haven't set up the table view to do that. We need to override the delegate method from UITableViewDelegate and UITableViewDataSource:
import UIKit
class HomeController: UITableViewController {
var photos = [Photo]()
func reloadPhotos() {
PhotoHelper.retrieveAll(onCompletion: { result in
self.photos = result
self.tableView.reloadData()
})
}
// Method 1: Decide height of each row
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UIScreen.main.bounds.width + 8 + 12 + 21 + 20
}
// Method 2: Decide number of rows (number of photos)
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return photos.count
}
// Method 3: Decide how to configure each row for display
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as! PhotoTableViewCell
let photo = photos[indexPath.row]
cell.photo = photo
cell.likesLabel.text = photo.likesToString
cell.photoView.image = UIImage(named: "Placeholder")
if let imageUrl = photo.imageUrl {
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
if let imageData = data {
DispatchQueue.main.async {
cell.photoView.image = UIImage(data: imageData)
}
}
}.resume()
}
return cell
}
@IBAction func uploadButtonTapped(_ sender: Any) {
presentImagePicker()
}
}
extension HomeController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func presentImagePicker() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.modalPresentationStyle = .popover
imagePicker.allowsEditing = false
imagePicker.sourceType = .photoLibrary
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage,
let resizedImageData = PhotoHelper.resize(image: pickedImage, maxWidth: 800, quality: 0.9) {
PhotoHelper.upload(imageData: resizedImageData, onCompletion: { succeeded in
if succeeded {
print("Upload succeeded")
} else {
print("Upload failed")
}
})
}
dismiss(animated: true, completion: {
})
}
}
Run your app again. You should be able to see the photos.
Now, your app will not automatically update the photos in the table view after you've uploaded, or after anyone in the app has uploaded a new photo.
We have to setup the app to receive notification from Skygear when there is an update. First, in AppDelegate.swift, do the following:
- Make it conforms to SKYContainerDelegate
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, SKYContainerDelegate {
- Register the device to receive notification
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
SKYContainer.default().configAddress("https://seanphotofeed.skygeario.com/")
SKYContainer.default().configure(withAPIKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx")
// Add the code below
SKYContainer.default().delegate = self
SKYContainer.default().registerDeviceCompletionHandler({ deviceID, error in
if let error = error {
print("Failed to register device: \(error)")
} else {
print("Registered device: \(deviceID)")
self.addSubscription(deviceID!)
}
})
application.registerUserNotificationSettings(UIUserNotificationSettings())
application.registerForRemoteNotifications()
// End of code to add
return true
}
- Add the SKYContainerDelegate methods
func container(_ container: SKYContainer!, didReceive notification: SKYNotification!) {
print("Received notification: \(notification)");
NotificationCenter.default.post(name: Notification.Name(rawValue: "SkygearNotificationReceived"), object: notification)
}
func addSubscription(_ deviceID: String) {
let query = SKYQuery(recordType: "photo", predicate: nil)
let subscription = SKYSubscription(query: query, subscriptionID: "my photos")
let operation = SKYModifySubscriptionsOperation(deviceID: deviceID, subscriptionsToSave: [subscription!])
operation?.deviceID = deviceID
operation?.modifySubscriptionsCompletionBlock = { (savedSubscriptions, operationError) in
DispatchQueue.main.async {
if let operationError = operationError {
print(operationError)
}
}
};
SKYContainer.default().publicCloudDatabase.execute(operation)
}
Then, finally, in HomeController.swift, we need to setup the logic on what to do after receiving notification from Skygear:
override func viewDidLoad() {
super.viewDidLoad()
reloadPhotos()
// Add the code below
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "SkygearNotificationReceived"), object: nil, queue: OperationQueue.main) { (notification) in
self.reloadPhotos()
}
}
Run the app again and upload a photo. You should see the table view being updated once your upload is completed.
Our app looks pretty complete now. One last thing to do: enable swipe to delete photos. iOS has a pretty robust API so it is super easy to implement this. We just need to override 2 more Delegate methods of the table view:
import UIKit
class HomeController: UITableViewController {
var photos = [Photo]()
func reloadPhotos() {
PhotoHelper.retrieveAll(onCompletion: { result in
self.photos = result
self.tableView.reloadData()
})
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UIScreen.main.bounds.width + 8 + 12 + 21 + 20
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return photos.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoCell", for: indexPath) as! PhotoTableViewCell
let photo = photos[indexPath.row]
cell.photo = photo
cell.likesLabel.text = photo.likesToString
cell.photoView.image = UIImage(named: "Placeholder")
if let imageUrl = photo.imageUrl {
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
if let imageData = data {
DispatchQueue.main.async {
cell.photoView.image = UIImage(data: imageData)
}
}
}.resume()
}
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let photo = photos[indexPath.row]
PhotoHelper.delete(photo: photo, onCompletion: { succeeded in
if succeeded {
self.photos.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
})
}
}
@IBAction func uploadButtonTapped(_ sender: Any) {
presentImagePicker()
}
}
extension HomeController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func presentImagePicker() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.modalPresentationStyle = .popover
imagePicker.allowsEditing = false
imagePicker.sourceType = .photoLibrary
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage,
let resizedImageData = PhotoHelper.resize(image: pickedImage, maxWidth: 800, quality: 0.9) {
PhotoHelper.upload(imageData: resizedImageData, onCompletion: { succeeded in
if succeeded {
print("Upload succeeded")
self.reloadPhotos()
} else {
print("Upload failed")
}
})
}
dismiss(animated: true, completion: {
})
}
}
Done! Our demo app is now completed.