A basic understanding of Swift and Node.js is needed to follow this tutorial. If you are a user of applications like Periscope, Instagram and Facebook, then you may have noticed they have a feature in their live streams where anytime someone likes the live content, the ‘likes’ float on your screen. This article will show you how you can implement the realtime floating hearts feature on your iOS application using Swift and Pusher. Here is a screen recording of what we will be achieving in this tutorial using Swift and Pusher. From the recording, you can see that when the like button is tapped, the likes float to the top and they are also replicated on another device viewing the video being played. Now, let’s begin the journey. Requirements To follow along in this tutorial you will need the following things: Knowledge of Swift and Xcode storyboards. Xcode installed on your machine. A Pusher application — you can create a free Pusher account . here Node.js and NPM installed on your machine. Cocoapods installed on your machine — install it using Ruby by running: . gem install cocoapods Hopefully, you have all the requirements checked off. Let’s get started. Creating the floating hearts application in Xcode Launch Xcode and in there create a new project. We are going to call the app (yes, it’s lame we know). When you are done with the initial setup, close Xcode and launch terminal. to the root directory of your application and run . This will create a in the root directory. Open it in your text editor and replace the code there with the following: streamlove cd pod init Podfile platform :ios, '9.0' target 'streamlove' do use_frameworks! pod 'PusherSwift', '~> 4.0' pod 'Alamofire', '~> 4.4' end After that save and close the file and run the command . This should start installing all the dependencies required for the application we are creating. Now open the file in Xcode. pod install streamlove.xcworkspace The next thing we need to do is design our applications storyboard. Open the file. We are going to add some mock views because we do not really want to implement anything on them and they are just there for the aesthetics. The main things we will focus on are the button and the background video. After designing our storyboard, this is what we hope to have: Main.storyboard In this storyboard, we have a button to the bottom right of the screen, and that button has an in the so you will need to to make the connection between the button and the . @IBAction ViewController ctrl+drag ViewController This should add the to the as shown below: @IBAction ViewController @IBAction func hearted(_ sender: Any) { // This function will be fired every time the button is tapped! } Creating a background looping video in iOS using Swift Next, we will create the video background that will just simulate a live stream (since creating an actual live stream falls far out of the scope of this article). Open the file and paste the following in it: ViewController import UIKit import PusherSwift import Alamofire class ViewController: VideoSplashViewController { override func viewDidLoad() { super.viewDidLoad() loadVideoStreamSample() } private func loadVideoStreamSample() { let url = NSURL.fileURL(withPath: Bundle.main.path(forResource: "video", ofType: "mp4")!) self.videoFrame = view.frame self.fillMode = .resizeAspectFill self.alwaysRepeat = true self.sound = true self.startTime = 0.0 self.duration = 10.0 self.alpha = 0.7 self.backgroundColor = UIColor.black self.contentURL = url self.restartForeground = true } override var prefersStatusBarHidden: Bool { return true } @IBAction func hearted(_ sender: Any) { // This function will be called everytime the button is tapped! } } In the first line, we have imported the libraries we will need, but mostly later in the tutorial. Now, let us focus on the others. The extends a that we have not yet created. In the method we have called a method and in that method, we are basically loading a video and setting some parameters for the video. These parameters will be implemented in the . ViewController VideoSplashViewController viewDidLoad loadVideoStreamSample VideoSplashViewController Now for the , we will be using a Swift library that is . However, because the library does not support Swift 3, we will be picking out the files we need and converting them to support Swift 3. The first one is the . Create a new file that extends and in there paste the following: VideoSplashViewController available on Github VideoSplashViewController VideoSplashViewController UIViewController import UIKit import MediaPlayer import AVKit public enum ScalingMode { case resize case resizeAspect case resizeAspectFill } public class VideoSplashViewController: UIViewController { private let moviePlayer = AVPlayerViewController() private var moviePlayerSoundLevel: Float = 1.0 public var videoFrame: CGRect = CGRect() public var startTime: CGFloat = 0.0 public var duration: CGFloat = 0.0 public var backgroundColor = UIColor.black { didSet { view.backgroundColor = backgroundColor } } public var contentURL: URL = URL(fileURLWithPath: "") { didSet { setMoviePlayer(url: contentURL) } } public var sound: Bool = true { didSet { moviePlayerSoundLevel = sound ? 1 : 0 } } public var alpha: CGFloat = 1 { didSet { moviePlayer.view.alpha = alpha } } public var alwaysRepeat: Bool = true { didSet { if alwaysRepeat { NotificationCenter.default.addObserver(forName:.AVPlayerItemDidPlayToEndTime, object:nil, queue:nil) { [weak self] (notification) in self?.playerItemDidReachEnd() } return } if !alwaysRepeat { NotificationCenter.default.removeObserver(self, name:.AVPlayerItemDidPlayToEndTime, object: nil) } } } public var fillMode: ScalingMode = .resizeAspectFill { didSet { switch fillMode { case .resize: moviePlayer.videoGravity = AVLayerVideoGravityResize case .resizeAspect: moviePlayer.videoGravity = AVLayerVideoGravityResizeAspect case .resizeAspectFill: moviePlayer.videoGravity = AVLayerVideoGravityResizeAspectFill } } } public var restartForeground: Bool = false { didSet { if restartForeground { NotificationCenter.default.addObserver(forName:.UIApplicationWillEnterForeground, object:nil, queue:nil) { [weak self] (notification) in self?.playerItemDidReachEnd() } } } } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) moviePlayer.view.frame = videoFrame moviePlayer.view.backgroundColor = self.backgroundColor; moviePlayer.showsPlaybackControls = false moviePlayer.view.isUserInteractionEnabled = false view.addSubview(moviePlayer.view) view.sendSubview(toBack: moviePlayer.view) } private func setMoviePlayer(url: URL){ let videoCutter = VideoCutter() videoCutter.cropVideoWithUrl(videoUrl: url, startTime: startTime, duration: duration) { [weak self] (videoPath, error) -> Void in guard let path = videoPath, let strongSelf = self else { return } strongSelf.moviePlayer.player = AVPlayer(url: path) strongSelf.moviePlayer.player?.addObserver(strongSelf, forKeyPath: "status", options: .new, context: nil) strongSelf.moviePlayer.player?.play() strongSelf.moviePlayer.player?.volume = strongSelf.moviePlayerSoundLevel } } public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let player = object as? AVPlayer else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } if player.status == .readyToPlay { movieReadyToPlay() } } deinit{ moviePlayer.player?.removeObserver(self, forKeyPath: "status") NotificationCenter.default.removeObserver(self) } // Override in subclass public func movieReadyToPlay() { } func playerItemDidReachEnd() { moviePlayer.player?.seek(to: kCMTimeZero) moviePlayer.player?.play() } func playVideo() { moviePlayer.player?.play() } func pauseVideo() { moviePlayer.player?.pause() } } Now create another file called that extends and paste in the following: VideoCutter NSObject import UIKit import AVFoundation extension String { var convert: NSString { return (self as NSString) } } public class VideoCutter: NSObject { /** Block based method for crop video url @param videoUrl Video url @param startTime The starting point of the video segments @param duration Total time, video length */ public func cropVideoWithUrl(videoUrl url: URL, startTime: CGFloat, duration: CGFloat, completion: ((_ videoPath:URL?, _ error: NSError?) -> Void)?) { DispatchQueue.global().async { let asset = AVURLAsset(url: url, options: nil) var outputPath = NSHomeDirectory() let documentPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) if (documentPaths.count > 0) { outputPath = documentPaths.first! } let fileManager = FileManager.default guard let exportSession = AVAssetExportSession(asset: asset, presetName: "AVAssetExportPresetHighestQuality") else { return } let outputFilePath = outputPath.convert.appendingPathComponent("output.mp4") if fileManager.fileExists(atPath: outputFilePath) { do { try fileManager.removeItem(atPath: outputFilePath) } catch let error { print(error) } } do { try fileManager.createDirectory(atPath:outputPath, withIntermediateDirectories: true, attributes: nil) } catch let error { print(error) } let start = CMTimeMakeWithSeconds(Float64(startTime), 600) let duration = CMTimeMakeWithSeconds(Float64(duration), 600) let range = CMTimeRangeMake(start, duration) let outputURL = URL(fileURLWithPath: outputFilePath) exportSession.outputURL = outputURL exportSession.timeRange = range exportSession.shouldOptimizeForNetworkUse = true exportSession.outputFileType = AVFileTypeMPEG4 exportSession.exportAsynchronously(completionHandler: { switch exportSession.status { case .completed: DispatchQueue.main.async { completion?(exportSession.outputURL, nil) } default: DispatchQueue.main.async { completion?(nil, nil) } } }) } } } In the files above we just created, it basically helps you manage the video background and sets a video of your choosing as the background video and loops it forever. This can also be useful for application landing pages. However, we are just using it for a make-believe video stream. 💡 If you are using a Swift version below 3, then you can use the source code as is in the repository, however, if you use Swift 3 or above you may need these modifications to make it work. Now the last thing we need to do is add an mp4 file to our workspace. You can use any mp4 file you wish to use. Drop the video file into the workspace and and added to the application target. video.mp4 make sure it is copied If you build and preview your application now you should see the video looping in the background. Great. Adding floating hearts to your iOS application Now that we have the video looping in the background, the next thing we will do is add the floating heart functionality to the application. Basically, every time someone clicks the heart button, a heart icon should float to the top and slowly disappear. Open the file and in the bottom right above the heart button, add a View with no background. This will be the viewport where the floating hearts will travel. You can make it a rectangle of about 250x350. Main.storyboard Next, we will be using another to add the floating hearts functionality to the application. The file we actually need is the file. The library does not yet have any package manager way to install it so we will be copying the contents of the file and adding it to a file in our workspace. library from Github Floater.swift We are building with Swift 3 so we need to make some modifications to the class, so copy and paste the code below if you are using Swift 3, and use as is if you are not. Create a new file and extend the object. Paste this into the class: Floater.swift UIView import UIKit @IBDesignable public class Floater: UIView { var image1: UIImage? var image2: UIImage? var image3: UIImage? var image4: UIImage? var isAnimating: Bool = false var views: [UIView]! var duration: TimeInterval = 1.0 var duration1: TimeInterval = 2.0 var duration2: TimeInterval = 2.0 var floatieSize = CGSize(width: 50, height: 50) var floatieDelay: Double = 10 var delay: Double = 10.0 var startingAlpha: CGFloat = 1.0 var endingAlpha: CGFloat = 0.0 var upwards: Bool = true var remove: Bool = true @IBInspectable var removeAtEnd: Bool = true { didSet { remove = removeAtEnd } } @IBInspectable var FloatingUp: Bool = true { didSet { upwards = FloatingUp } } @IBInspectable var alphaAtStart: CGFloat = 1.0 { didSet { startingAlpha = alphaAtStart } } @IBInspectable var alphaAtEnd: CGFloat = 0.0 { didSet { endingAlpha = alphaAtEnd } } @IBInspectable var rotationSpeed: Double = 10 { didSet { duration2 = 20 / rotationSpeed } } @IBInspectable var density: Double = 10 { didSet { floatieDelay = 1 / density } } @IBInspectable var delayedStart: Double = 10 { didSet { delay = delayedStart } } @IBInspectable var speedY: CGFloat = 10 { didSet { duration = Double(10/speedY) } } @IBInspectable var speedX: CGFloat = 5 { didSet { duration1 = Double(10/speedX) } } @IBInspectable var floatieWidth: CGFloat = 50 { didSet { floatieSize.width = floatieWidth } } @IBInspectable var floatieHeight: CGFloat = 50 { didSet { floatieSize.height = floatieHeight } } @IBInspectable var borderColor: UIColor = UIColor.clear { didSet { layer.borderColor = borderColor.cgColor } } @IBInspectable var borderWidth: CGFloat = 0 { didSet { layer.borderWidth = borderWidth } } @IBInspectable var cornerRadius: CGFloat = 0 { didSet { layer.cornerRadius = cornerRadius } } @IBInspectable var floaterImage1: UIImage? { didSet { image1 = floaterImage1 } } @IBInspectable var floaterImage2: UIImage? { didSet { image2 = floaterImage2 } } @IBInspectable var floaterImage3: UIImage? { didSet { image3 = floaterImage3 } } @IBInspectable var floaterImage4: UIImage? { didSet { image4 = floaterImage4 } } override public func awakeFromNib() { super.awakeFromNib() } func startAnimation() { print("Start Animating") isAnimating = true views = [] var imagesArray = [UIImage?]() var actualImages = [UIImage]() let frameW = self.frame.width let frameH = self.frame.height var startingPoint: CGFloat! var endingPoint: CGFloat! if upwards { startingPoint = frameH endingPoint = floatieHeight*2 } else { startingPoint = 0 endingPoint = frameH - floatieHeight*2 } imagesArray += [image1, image2, image3, image4] if !imagesArray.isEmpty { for i in imagesArray { if i != nil { actualImages.append(i!) } } } let deadlineTime = DispatchTime.now() + .seconds(Int(self.delay * Double(NSEC_PER_SEC))) DispatchQueue.global().asyncAfter(deadline: deadlineTime, execute: { var goToNext = true while self.isAnimating { if goToNext { goToNext = false DispatchQueue.main.asyncAfter(deadline: .now()+0.3, execute: { let randomNumber = self.randomIntBetweenNumbers(firstNum:1, secondNum: 2) var randomRotation: CGFloat! if randomNumber == 1 { randomRotation = -1 } else { randomRotation = 1 } let randomX = self.randomFloatBetweenNumbers(firstNum: 0 + self.floatieSize.width/2, secondNum: self.frame.width - self.floatieSize.width/2) let floatieView = UIView(frame: CGRect(x: randomX, y: startingPoint, width: 50, height: 50)) self.addSubview(floatieView) let floatie = UIImageView(frame: CGRect(x: 0, y: 0, width: self.floatieSize.width, height: self.floatieSize.height)) if !actualImages.isEmpty { let randomImageIndex = (self.randomIntBetweenNumbers(firstNum: 1, secondNum: actualImages.count) - 1 ) floatie.image = actualImages[randomImageIndex] floatie.center = CGPoint(x: 0, y: 0) floatie.backgroundColor = UIColor.clear floatie.layer.zPosition = 10 floatie.alpha = self.startingAlpha floatieView.addSubview(floatie) var xChange: CGFloat! if randomX < self.frame.width/2 { xChange = randomX + self.randomFloatBetweenNumbers(firstNum: randomX, secondNum: frameW-randomX) } else { xChange = self.randomFloatBetweenNumbers(firstNum: self.floatieSize.width*2, secondNum: randomX) } self.views.append(floatieView) UIView.animate(withDuration: self.duration, delay: 0, options: [], animations: { floatieView.center.y = endingPoint floatie.alpha = self.endingAlpha goToNext = false }, completion: {(value: Bool) in if self.remove { floatieView.removeFromSuperview() } }) UIView.animate(withDuration: self.duration1, delay: 0, options: [.repeat, .autoreverse], animations: { floatieView.center.x = xChange }, completion: nil) UIView.animate(withDuration: self.duration2, delay: 0, options: [.repeat, .autoreverse], animations: { floatieView.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2)*randomRotation) }, completion: nil) } }) } } }) } func stopAnimation() { print("Stop Animating") views = [] isAnimating = false if !views.isEmpty { for i in views { i.removeFromSuperview() } } } func randomFloatBetweenNumbers(firstNum: CGFloat, secondNum: CGFloat) -> CGFloat{ return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(firstNum - secondNum) + min(firstNum, secondNum) } func randomIntBetweenNumbers(firstNum: Int, secondNum: Int) -> Int{ return firstNum + Int(arc4random_uniform(UInt32(secondNum - firstNum + 1))) } } The library simply creates a floating heart when the method is called and stops it when the method is called. Now that the file is created, open your file and add the View to the floater view we created earlier. This should add some new options in the side bar. These options are due to and that were added to the class. startAnimation stopAnimation Main.storyboard Floater.swift @IBDesignable @IBInspectable Floater.swift 💡 _**IBDesignable**_ and _**IBInspectable**_ , a way to create custom elements and the attributes. This can be directly added to the iOS Interface Builder. Read more about IBDesignable and IBInspectable . Now in the new options fields, add the following values: For the floater image, add a 30x30 heart image to your workspace and then select it in the floater image section. Now open the and add the following methods: ViewController @IBOutlet weak var floaterView: Floater! private func startEndAnimation() { floaterView.startAnimation() DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { self.floaterView.stopAnimation() }) } Now call the from the method so it is invoked when the button is pressed. Make sure the is linked to the view port that we created earlier in the article. Now, when you build and preview, you should see the heart floating every time the button is clicked. startEndAnimation hearted @IBOutlet floaterView Adding realtime functionality to our floating hearts using Pusher Now that we have successfully added the floating hearts, the next thing to do is add realtime functionality using Pusher. If you have not already, , create a new application and copy the credentials as you will need them. create a Pusher account Open the and in there add the following: ViewController static let API_ENDPOINT = "http://localhost:4000"; var pusher : Pusher! let deviceUuid : String = UIDevice.current.identifierForVendor!.uuidString private func listenForNewLikes() { pusher = Pusher(key: "PUSHER_KEY", options: PusherClientOptions(host: .cluster("PUSHER_CLUSTER"))) let channel = pusher.subscribe("likes") let _ = channel.bind(eventName: "like", callback: { (data: Any?) -> Void in if let data = data as? [String: AnyObject] { let uuid = data["uuid"] as! String if uuid != self.deviceUuid { self.startEndAnimation() } } }) pusher.connect() } private func postLike() { let params: Parameters = ["uuid": deviceUuid] Alamofire.request(ViewController.API_ENDPOINT + "/like", method: .post, parameters: params).validate().responseJSON { response in switch response.result { case .success: print("Liked") case .failure(let error): print(error) } } } First, we define some class properties for storing the API endpoint base URL, the Pusher instance and the device UUID. The is where we define a listener that waits for events sent from Pusher and then fires a callback when it receives the event. We will be using that to trigger the floating hearts method. The method is where we use to hit an endpoint (we will create this next). The endpoint will be where we send “like” events to Pusher so they can be broadcast to other listeners on the channel. listenForNewLikes startAndEndAnimation postLike AlamoFire If all is well, your should now look like this: ViewController import UIKit import PusherSwift import Alamofire class ViewController: VideoSplashViewController { @IBOutlet weak var floaterView: Floater! static let API_ENDPOINT = "http://localhost:4000"; var pusher : Pusher! let deviceUuid : String = UIDevice.current.identifierForVendor!.uuidString override func viewDidLoad() { super.viewDidLoad() loadVideoStreamSample() listenForNewLikes() } @IBAction func hearted(_ sender: Any) { postLike() startEndAnimation() } private func startEndAnimation() { floaterView.startAnimation() DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { self.floaterView.stopAnimation() }) } private func listenForNewLikes() { pusher = Pusher(key: "PUSHER_KEY", options: PusherClientOptions(host: .cluster("PUSHER_CLUSTER"))) let channel = pusher.subscribe("likes") let _ = channel.bind(eventName: "like", callback: { (data: Any?) -> Void in if let data = data as? [String: AnyObject] { let uuid = data["uuid"] as! String if uuid != self.deviceUuid { self.startEndAnimation() } } }) pusher.connect() } private func postLike() { let params: Parameters = ["uuid": deviceUuid] Alamofire.request(ViewController.API_ENDPOINT + "/like", method: .post, parameters: params).validate().responseJSON { response in switch response.result { case .success: print("Liked") case .failure(let error): print(error) } } } private func loadVideoStreamSample() { let url = NSURL.fileURL(withPath: Bundle.main.path(forResource: "video", ofType: "mp4")!) self.videoFrame = view.frame self.fillMode = .resizeAspectFill self.alwaysRepeat = true self.sound = true self.startTime = 0.0 self.duration = 10.0 self.alpha = 0.7 self.backgroundColor = UIColor.black self.contentURL = url self.restartForeground = true } override var prefersStatusBarHidden: Bool { return true } } ⚠️ You should replace the “PUSHER_CLUSTER” and “PUSHER_KEY” with the actual values gotten from your Pusher application dashboard. That should do it for the Xcode side of things. Now we need to create a backend application for our application. This backend app will just receive the payload from the application and send it to Pusher. Creating the Node.js backend for our realtime floating hearts app Create a directory for the web application and then create two new files: The file… index.js let Pusher = require('pusher'); let express = require('express'); let app = express(); let bodyParser = require('body-parser') let pusher = new Pusher(require('./config.js')['config']); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.post('/like', (req, res, next) => { let payload = {uuid: req.body.uuid} pusher.trigger('likes', 'like', payload) res.json({success: 200}) }) app.get('/', (req, res) => { res.json("It works!"); }); app.use((req, res, next) => { let err = new Error('Not Found'); err.status = 404; next(err); }); app.listen(4000, function() { console.log('App listening on port 4000!') }); The file also has one route where it receives messages from the iOS application and triggers the Pusher event which is picked up by the application. index.js The next file is the where we define the NPM dependencies: packages.json { "main": "index.js", "dependencies": { "body-parser": "^1.16.0", "express": "^4.14.1", "pusher": "^1.5.1" } } Finally, we have the where we will put our Pusher configuration: config.js module.exports = { appId: 'PUSHER_ID', key: 'PUSHER_KEY', secret: 'PUSHER_SECRET', cluster: 'PUSHER_CLUSTER', }; Now run on the directory and then once the npm installation is complete. You should see message. npm install node index.js App listening on port 4000! Testing our floating hearts application Once you have your local node web server running, you will need to make some changes so your application can talk to the local web server. In the file, make the following changes: info.plist With this change, you can build and run your application and it will talk directly with your local web application. Conclusion In this article, we have been able to replicate the floating hearts feature that apps like Facebook, Instagram, Periscope etc have. You can use this as a base to expand the actual feature into a working application of your own. This Post was first Published on Pusher