Build a photo feed with Swift and Raspberry Pi
A basic understanding of Swift and Python is needed to follow this tutorial.
The Camera Module is a great accessory for the Raspberry Pi, which itself is a great small device that can be easily hidden, making both ideal for a security camera.
In this tutorial, we’re going to build the iOS photo feed with a Python script to take pictures at a certain interval with the Raspberry Pi camera, then save them to a web server and publish the URL to a Pusher channel so an iOS app can show them in a realtime photo feed.
You’ll need:
- A Raspberry Pi 3 with the latest Raspbian
- A Raspberry Pi Camera Module
- ngrok
- Xcode 8
The complete source code of the Python script and the iOS app is on Github for reference. This is how the final app will look:
Let’s get started!
Configuring Raspberry Pi
First, with the Pi switched off, you need to connect the Camera Module to the Raspberry Pi’s camera port, then start up the Pi and ensure the software is enabled. Here’s a great getting started guide that shows you how.
Next, make sure your OS is updated by executing the following commands:
sudo apt-get update
sudo apt-get upgrade
Next, you need to make sure all the development dependencies we’re going to need are installed, by executing:
sudo apt-get install build-essential libssl-dev python-dev libffi-dev python3-dev python3-pip
It’s a good practice to use virtualenv so multiple Python projects that have different (and conflicting) requirements can coexist on the same computer. If you don’t have already, install it with:
sudo pip install virtualenv
Now install a web server. NGINX is a good option:
sudo apt-get install nginx
sudo /etc/init.d/nginx start
We’ll need a directory on the web server to store the pictures. Let’s create the directory photos
under /var/www/html/
and give it permissions (by changing the owner to the user pi
(or the user you’ll use to run the Python script):
sudo mkdir /var/www/html/photos
sudo chown -R pi:pi /var/www/html/photos
Finally, we’re going to use ngrok to make our web server publicly available. Download the Linux ARM version:
In another terminal window, unzip the file and run it with:
./ngrok http 80
This will create a secure tunnel to your Raspberry Pi’s port 80
and a screen like the following will be shown:
Copy the URL with the HTTPS protocol (in this case https://5e34f8a7.ngrok.io
). This is your public URL and we’re going to use it later (we’ll use HTTPS because iOS cannot load HTTP resources by default, only HTTPS resources, which is a good practice anyway).
Setting up a Pusher Channels app
Create a free account with Pusher.
When you first log in, you’ll be asked to enter some configuration options to create your app:
Enter a name, choose iOS as your frontend tech, and Python as your backend tech. This will give you some sample code to get you started:
But don’t worry, this won’t lock you into this specific set of technologies as you can always change them. With Pusher, you can use any combination of libraries.
Then go to the App Keys tab to copy your App ID, Key, and Secret credentials, we’ll need them later.
The Python script
Create a new directory, cd
into it, and create a virtual environment for the project with Python 3:
virtualenv -p python3 env
And activate it with:
source env/bin/activate
With this, you’ll get the Python binary inside virtualenv
instead of the global one. This way, install the dependencies of the script, Pusher and PiCamera:
pip install pusher
pip install picamera
Now, for the script, let’s start with this version:
from picamera import PiCamera
from time import sleep
camera = PiCamera()
# If you need to rotate the camera
# camera.rotation = 180
camera.start_preview()
sleep(30)
camera.stop_preview()
Save the file as camera.py
And run it with:
python camera.py
This will show the camera in preview mode (only when a monitor is connected to the Pi) for 30
seconds so you can position the camera the way you want it.
Now for the real script, let’s import the dependencies:
from picamera import PiCamera
from time import sleep
from datetime import datetime
from pusher import Pusher
...
Enter the time between photos in seconds (in this case 15
), and your Pusher app information:
...
seconds_between_photos = 15
pusher_app_id = '<INSERT_YOUR_PUSHER_APP_ID_HERE>'
pusher_key = '<INSERT_YOUR_PUSHER_APP_KEY_HERE>'
pusher_secret = '<INSERT_YOUR_PUSHER_APP_SECRET_HERE>'
hostname = '<INSERT_YOUR_NGROK_HTTPS_URL_HERE>'
camera = PiCamera()
pusher = Pusher(pusher_app_id, pusher_key, pusher_secret)
Set the resolution of the camera (in this case 640x480
will do):
...
# If you need to rotate the camera
# camera.rotation = 180
camera.resolution = (640, 480)
And in an infinite loop, format the actual time as a string (like 03-23-2017-11:18:55
) to annotate the picture and use it as the name of the image file so we can publish the URL to Pusher:
...
while True:
try:
sleep(seconds_between_photos)
date = datetime.now().strftime('%m-%d-%Y-%H:%M:%S')
camera.annotate_text = date
filename = '/photos/' + date + '.jpg'
camera.capture('/var/www/html' + filename)
url = hostname + filename
pusher.trigger('photos', 'new_photo', {'url': url})
except Exception as e:
print ('Error:', e)
In this simple implementation, we’ll catch any error (like timeouts if the Pi lost the network connectivity) so the script couldn’t be stopped when one is thrown.
And that’s it. If you run it, you should see images files being saved in the /var/www/html/photos
directory.
Setting up the Xcode project
Open Xcode and create a Single View Application:
Give it a name, choose Swift as the language and Universal in the Devices option:
Now, we’re going to install the project dependencies with CocoaPods. Close your Xcode project, and in a terminal window go to the top-level directory of your project and execute this command:
pod init
This will create a text file named Podfile
with some defaults, open it and add as dependencies PusherSwift
and Kingfisher
(a library for downloading and caching images from the web). It should look like this:
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'photo-feed-ios' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for twitter_feed_pusher
pod 'PusherSwift'
pod 'Kingfisher'
end
Now you can install the dependencies in your project with:
pod install
And from now on, make sure to open the generated Xcode workspace instead of the project file:
open photo-feed-ios.xcworkspace
For apps like this, creating everything programmatically is easier, so we won’t use the Interface Builder or the storyboard file that Xcode creates (Main.storyboard
).
The iOS app
Let’s start by opening the file ViewController.swift
that Xcode created for us and change it so it extends from a UITableViewController
:
import UIKit
class ViewController: UITableViewController {
...
}
Now open AppDelegate.swift
to manually create the window in which our app is going to live and set an instance of the ViewController
as the rootViewController
of the window:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController =
UINavigationController(rootViewController: ViewController())
return true
}
...
}
If you run the app at this point, you should see something like the following:
All right, let’s create a new Swift file, Photo.swift
, to create a structure that will hold the photo’s information (right now just its URL):
import Foundation
struct Photo {
let url: String
}
We’re going to need a custom cell class for our UITableView
. Once again, create a new Swift file, this time with the name PhotoCell.swift
and the following content:
import UIKit
class PhotoCell : UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The layout for the cell is going to have only a UIImageView
for the photo, which will be scaled to fill the entire cell:
class PhotoCell : UITableViewCell {
let photoImage: UIImageView = {
let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.contentMode = UIViewContentMode.scaleToFill
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
...
}
It’s important to set translatesAutoresizingMaskIntoConstraints
to false
because we’re going to use the NSLayoutAnchor API to position the view:
class PhotoCell : UITableViewCell {
...
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(photoImage)
photoImage.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
photoImage.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
photoImage.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
photoImage.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
selectionStyle = .none
}
...
}
The above code will add the UIImageView
to the cell and make it fill the whole cell space and disable the selection of the cell.
Now let’s add a property observer so when an object of type Photo
is set on this cell, we can set the image (don’t forget to import Kingfisher
):
import UIKit
import Kingfisher
class PhotoCell : UITableViewCell {
var photo: Any? {
didSet {
guard let p = photo as? Photo else { return }
// Create the image URL and set it with Kingfisher
let imageUrl = URL(string: p.url)
photoImage.kf.indicatorType = .activity
photoImage.kf.setImage(
with: imageUrl,
placeholder: #imageLiteral(resourceName: "placeholder"),
options: [.transition(.fade(0.5))]
)
}
}
...
}
With the above code, Kingfisher will show an activity indicator while downloading the image, showing a placeholder image in the meantime, and adding a fade transition when it’s time to show the image. In the GitHub app, you can find in Assets.xcassets
the logo of Pusher as the placeholder image, but you can add your own image.
Now, in the ViewController
class, let’s create a cell identifier, an array to hold our photos and the Pusher object:
import UIKit
import PusherSwift
class ViewController: UITableViewController {
let cellId = "cellId"
var photos= [Photo]()
var pusher: Pusher! = nil
...
}
Inside the viewDidLoad
method, let’s give our app a title and register our custom cell:
class ViewController: UITableViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Pusher Photo Feed"
tableView.register(PhotoCell.self,
forCellReuseIdentifier: cellId)
}
}
We will listen to new images by setting up Pusher passing our app key, subscribing to the channel photos
and binding the event new_photo
in the following way:
class ViewController: UITableViewController {
...
override func viewDidLoad() {
...
pusher = Pusher(
key: "<INSERT_YOUR_PUSHER_KEY_HERE>"
)
let channel = pusher.subscribe("photos")
let _ = channel.bind(eventName: "new_photo", callback: { (data: Any?) -> Void in
if let data = data as? [String : AnyObject] {
let url = data["url"] as! String
let photo = Photo(url: url)
self.photos.insert(photo, at: self.photos.startIndex)
let indexPath = IndexPath(row: 0, section: 0)
self.tableView.insertRows(at: [indexPath],
with: UITableViewRowAnimation.automatic)
self.tableView.scrollToRow(at: indexPath,
at: UITableViewScrollPosition.none, animated: true)
}
})
pusher.connect()
}
}
This way, when a new photo comes in, we can extract its URL, create a Photo
instance and insert it in the array and in the tableView
to display it.
Of course, for this to happen, we also need to implement the following methods so the tableView
knows how many items it has to display and in which cell:
class ViewController: UITableViewController {
...
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: cellId, for: indexPath) as! PhotoCell
// At this point, the didSet block will set up the cell's views
cell.photo = photos[indexPath.item]
return cell;
}
}
Finally, we need to give the cells a height (of 480
) with the following method:
class ViewController: UITableViewController {
...
override func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
return 480
}
}
And we’re done! Now you have an iOS photo feed that is connected to a Raspberry Pi camera and updates in realtime.
Testing the app
On the Raspberry Pi, with your virtual environment activated, execute the Python process with (don’t forget to start ngrok and replace your HTTPS URL in the script):
python camera.py
If you only want to test the app, you can use the Pusher Debug Console on your dashboard (just remember to use URLs with HTTPS, because iOS cannot load HTTP resources by default):
When a new_photo
event is received in the Pusher channel, the new photo will come up in the iOS app:
In landscape, the photos look better:
Conclusion
You can find the final version of the backend here and the final version of the iOS app here.
Hopefully, this tutorial has shown you how to build a realtime iOS app with Pusher in an easy way. You can improve the app by changing the layout, showing more information or saving it to a database. Or, using OpenCV, take pictures only with motion is detected by the Raspberry Pi camera.
Remember that your forever free Pusher account includes 100 connections, unlimited channels, 200k daily messages, SSL protection, and there are more features than just Pub/Sub Messaging. Sign up here.
Further reading
- Pusher Swift client
- Installing Raspbian with NOOBS
- Apache vs Nginx: Practical Considerations
- Setting up a web server on a Raspberry Pi
- A non-magical introduction to Pip and Virtualenv for Python beginners
- Getting started with picamera
- Kingfisher Cheat Sheet
- Pain Free Constraints with Layout Anchors
- Property observers: didSet
25 April 2017
by Esteban Herrera