Swift Tutorial: Build a Facebook Album Browser - SELISE

Swift Tutorial: Build a Facebook Album Browser

July 9, 2014

Welcome to our third Swift tutorial. I hope that, by now, you have a good grasp at using String, Collection and other basic operations in Swift. Today, we’ll take it one step further and learn to develop our first iOS app (Facebook Album Browser) using Swift.

What we hope to have learnt by the end of this particular tutorial is basically how to develop an application as shown below.

In order to develop this application, we need to know a few things. For example:

  • How to work in Facebook SDK for iOS
  • How to work with Objective C Framework/Library in Swift
  • How to work with TableView in Swift (please refer to my first article on TableView in Swift)
  • Notification API in iOS (How to use Notification system in Swift. The notification system is like a “Publish/Subscribe” messaging system.)
  • Some basic ideas about Storyboard and MVC.

That is more or less about it. Clearly, the Facebook Album Browser is not the most fancy or complex stuff you can make with Swift. The reason why we’ll go through this tutorial is that it should provide a strong foundation that will eventually help us to write our own Facebook related application in iOS.

So without further ado, let’s jump directly into our main focus- the app development. We’ll go slow and easy, step by step.

Step 1:

Our first priority is to create a “Single View Application” and select Swift as the desired language. So now we should have a default Storyboard and a “ViewController” with a single view. Now, let’s open our Storyboard. Let’s keep it simple and go for a design like this one:

I’ll admit it- this is not a very good UI since I’m not a great designer, but this should do for now. Here we have two buttons; one is the “Login to Facebook” button and the other being the “Fetch Albums” button.

So let’s break down the functionality of the page we just designed. First, one has to login into their respective accounts, after which the option of fetching albums from your profile becomes open. Doing this will load the album underneath the “TableView”. Very straightforward so far.

When a user clicks the “Login to Facebook” button, it will use the Facebook SDK and will make a login to facebook on behalf of the user. After a successful login, the user’s profile picture will be loaded and the “Login to Facebook” button will be replaced by a “Logout” label. But before anything else, let’s see how we can use “Facebook SDK” inside iOS.

Let us first download Facebook SDK from facebook developer site, which has been developed to work with “Objective C”. However, please do realize that Swift and Objective C are different languages. Hence, we have to follow certain procedures in order to use the framework.

After having downloaded the framework, drag it onto your project as shown below:

app_structure

Since this was built for working with Objective C, there is a simple little trick that we need to do to get around it-  right click on the FBApp folder and select New File; after this, proceed to select an Objective C File. This should prompt a notification as to whether we want to create a Bridge Header, upon which we must select yes.

So why is XCode doing this? Since this project is built to be used on Swift, any Objective C file can also be used in it; given you just add a “Bridge Header”.

So far so good? Now we simply delete the newly created Objective C file, keeping the “Bridge Header” file as it is (the image above may help). We will see a “FBApp-Bridge-Header.h”. Now let’s open this file and write down the code below:

#import <FacebookSDK/FacebookSDK.h>

This should automatically expose all the Facebook related class to your Swift.

Step 2:

To use the Facebook SDK, we also have to put some information in the “application plist” file. Please visit the Facebook developer site and see how to use this SDK. It is very simple and shouldn’t take more than 15 minutes of your time to understand how it works.

Step 3:

As we will use the Facebook SDK, it’s a good practice to keep all the facebook SDK related functionalities under one class. This is a kind of domain specific encapsulation. I prefer to create a helper class for this called “FBHelper”. This helper class is responsible for doing all the facebook related activities inside your app such as “Login”, “Logout”, “Share”, “Post”, etc. See the code below:

//
//  FBHelper.swift
//  FBApp
//
//  Created by Md. Arifuzzaman Arif on 7/4/14.
//  Copyright (c) 2014 Md. Arifuzzaman Arif. All rights reserved.
//
import Foundation

class FBHelper{
var fbSession:FBSession?;
init(){
self.fbSession = nil;
}

func fbAlbumRequestHandler(connection:FBRequestConnection!, result:AnyObject!, error:NSError!){

if let gotError = error{
println(gotError.description);
}
else{
let graphData = result.valueForKey("data") as Array;
var albums:AlbumModel[] =  AlbumModel[]();
for obj:FBGraphObject in graphData{
let desc = obj.description;
println(desc);
let name = obj.valueForKey("name") as String;
println(name);
if(name == "ETC"){
let test="";
}
let id = obj.valueForKey("id") as String;
var cover = "";
if let existsCoverPhoto : AnyObject = obj.valueForKey("cover_photo"){
let coverLink = existsCoverPhoto  as String;
cover = "/(coverLink)/photos";
}

//println(coverLink);
let link = "/(id)/photos";

let model = AlbumModel(name: name, link: link, cover:cover);
albums.append(model);

}
NSNotificationCenter.defaultCenter()?.postNotificationName("albumNotification", object: nil, userInfo: ["data":albums]);
}
}

func fetchPhoto(link:String){
let fbRequest = FBRequest.requestForMe();
fbRequest.graphPath = link;
fbRequest.startWithCompletionHandler(fetchPhotosHandler);
}

func fetchPhotosHandler(connection:FBRequestConnection!, result:AnyObject!, error:NSError!){
if let gotError = error{

}
else{
var pictures:UIImage[] = UIImage[]();
let graphData = result.valueForKey("data") as Array;
var albums:AlbumModel[] =  AlbumModel[]();
for obj:FBGraphObject in graphData{
println(obj.description);
let pictureURL = obj.valueForKey("picture") as String;
let url = NSURL(string: pictureURL);
let picData = NSData(contentsOfURL: url);
let img = UIImage(data: picData);
pictures.append(img);
}

NSNotificationCenter.defaultCenter().postNotificationName("photoNotification", object: nil, userInfo: ["photos":pictures]);
}
}

func fetchAlbum(){

let request =  FBRequest.requestForMe();
request.graphPath = "me/albums";

request.startWithCompletionHandler(fbAlbumRequestHandler);
}

func logout(){
self.fbSession?.closeAndClearTokenInformation();
self.fbSession?.close();
}

func login(){
let activeSession = FBSession.activeSession();
let fbsessionState = activeSession.state;
if(fbsessionState.value != FBSessionStateOpen.value && fbsessionState.value != FBSessionStateOpenTokenExtended.value){

let permission = ["basic_info", "email","user_photos","friends_photos"];

FBSession.openActiveSessionWithPublishPermissions(permission, defaultAudience: FBSessionDefaultAudienceFriends, allowLoginUI: true, completionHandler: self.fbHandler);

}
}

func fbHandler(session:FBSession!, state:FBSessionState, error:NSError!){
if let gotError = error{
//got error
}
else{

self.fbSession = session;

FBRequest.requestForMe()?.startWithCompletionHandler(self.fbRequestCompletionHandler);
}
}

func fbRequestCompletionHandler(connection:FBRequestConnection!, result:AnyObject!, error:NSError!){
if let gotError = error{
//got error
}
else{
//let resultDict = result as Dictionary;
//let email = result["email"];
//let firstName = result["first_name"];

let email : AnyObject = result.valueForKey("email");
let firstName:AnyObject = result.valueForKey("first_name");
let userFBID:AnyObject = result.valueForKey("id");
let userImageURL = "https://graph.facebook.com/(userFBID)/picture?type=small";

let url = NSURL.URLWithString(userImageURL);

let imageData = NSData(contentsOfURL: url);

let image = UIImage(data: imageData);

println("userFBID: (userFBID) Email (email) n firstName:(firstName) n image: (image)");

var userModel = User(email: email, name: firstName, image: image);

NSNotificationCenter.defaultCenter().postNotificationName("PostData", object: userModel, userInfo: nil);

}
}
}

We also need couples of Model class to hold the facebook information:

  1. FBUserModel.swift
  2. AlbumModel.swift

See the code below:

User

//
//  FBUserModel.swift
//  FBApp
//
//  Created by Md. Arifuzzaman Arif on 7/4/14.
//  Copyright (c) 2014 Md. Arifuzzaman Arif. All rights reserved.
//
import Foundation
class User{
let email:AnyObject = "";
let name:AnyObject = "";
let image:UIImage;

init(email:AnyObject, name:AnyObject, image:UIImage){
self.image = image
self.name = name;
self.email=email;
}
}

AlbumModel

//  AlbumModel.swift
//  FBApp
//
//  Created by Md. Arifuzzaman Arif on 7/5/14.
//  Copyright (c) 2014 Md. Arifuzzaman Arif. All rights reserved.
//
import Foundation
class AlbumModel{
let name = "";
let link = "";
let cover = "";
init(name:String, link:String, cover:String){
self.name = name;
self.link = link;
self.cover = cover;
}
}

All we need now is to call the FBHelper class and extract the information. Please see the “ViewController.swift”

//
//  ViewController.swift
//  FBApp
//
//  Created by Md. Arifuzzaman Arif on 7/4/14.
//  Copyright (c) 2014 Md. Arifuzzaman Arif. All rights reserved.
//
import UIKit

class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {

let fbHelper = FBHelper();
var sources:AlbumModel[] = AlbumModel[]();
var currentAlbumModel = AlbumModel(name: "", link: "", cover:"");
var destController:AlbumViewController?;

@IBOutlet var albumTable : UITableView
@IBOutlet var imgProfile : UIImageView

@IBAction func fetchDataAction(sender : AnyObject) {
fbHelper.fetchAlbum();
}
@IBOutlet var btnLoginLogout : UIButton

@IBAction func facebookLogoutAction(sender : AnyObject) {
self.fbHelper.logout();
self.btnLoginLogout.titleLabel.text = "Login to Facebook";
}
@IBAction func facebookLoginAction(sender : AnyObject) {

if(self.btnLoginLogout.titleLabel.text == "Login to Facebook"){
fbHelper.login();
}
else{
fbHelper.logout();
}

}

func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!)
{
self.currentAlbumModel = self.sources[indexPath.row];
if(self.destController){
self.destController!.albumModel = self.currentAlbumModel;
self.destController!.fbHelper = self.fbHelper;
self.destController!.executePhoto();
}

}

func selectRowAtIndexPath(indexPath: NSIndexPath!, animated: Bool, scrollPosition: UITableViewScrollPosition){

}

func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!{
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell1") as UITableViewCell;
let data = self.sources[indexPath.row];
cell.textLabel.text = data.name;
cell.detailTextLabel.text = data.link;
if(data.cover != ""){
let coverPhotoURL = NSURL(string: data.cover);
let coverPhotoData = NSData(contentsOfURL: coverPhotoURL);

cell.imageView.image = UIImage(data: coverPhotoData);

}
return cell;
}

func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int{
return self.sources.count;
}

func executeAlbum(notification:NSNotification){
let data = notification.userInfo.objectForKey("data") as AlbumModel[];
self.sources = data;
self.albumTable.reloadData();
}

func executeHandle(notification:NSNotification){
let userData = notification.object as User;

let name = userData.name as String;
let email = userData.email as String;
//lblName.text = name;
//lblEmail.text = email;
imgProfile.image = userData.image;
self.btnLoginLogout.titleLabel.text = "Logout";

}

override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if(segue.identifier == "photoSegue"){
let destinitionController = segue.destinationViewController as AlbumViewController;
destinitionController.albumModel = self.currentAlbumModel;
self.destController = destinitionController;
}
}

override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: "PostData", object: nil);
NSNotificationCenter.defaultCenter().removeObserver(self, name: "albumNotification", object: nil);
}

override func viewDidLoad() {

NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("executeHandle:"), name: "PostData", object: nil);
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("executeAlbum:"), name: "albumNotification", object: nil);

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

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

}

This will load the album and other things on the main screen.

Step 4:

To show photos of each album, we would need another screen with its own controller. In this case, I have used “AlbumViewController” as a sub class of “UITableViewController”.

Just looking at this view, you’d know that its ready made. You just need to drag a “UITableViewController” in the storyboard to achieve this. Please refer back to my first tutorial if you want to know more details about table view.

View controller code of this view is given below:

//
//  AlbumViewController.swift
//  FBApp
//
//  Created by Md. Arifuzzaman Arif on 7/5/14.
//  Copyright (c) 2014 Md. Arifuzzaman Arif. All rights reserved.
//

import Foundation
class AlbumViewController:UITableViewController{

var albumModel:AlbumModel = AlbumModel(name: "", link: "", cover:"");
var fbHelper:FBHelper?
var sources:UIImage[] = UIImage[]();
var singlePhotoViewController:SinglePhotoViewController?;

func photoExecuted(notification:NSNotification){
let photos = notification.userInfo.valueForKey("photos") as UIImage[];
self.sources = photos;
self.tableView.reloadData();
}

override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
let img = self.sources[indexPath.row];
return img.size.height;
}
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {

var cell = tableView.dequeueReusableCellWithIdentifier("Cell1") as UITableViewCell;

//cell.textLabel.text = self.sources[indexPath.row] as String;
//let imageView = UIImageView(frame: CGRectMake(2, 2, 100, 100));
//imageView.image = self.sources[indexPath.row];
//cell.contentView.addSubview(imageView);
cell.imageView.image = self.sources[indexPath.row];
return cell;
}

override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return self.sources.count;
}

override func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
let photo = self.sources[indexPath.row];
if(self.singlePhotoViewController){
self.singlePhotoViewController!.photo = photo;
//self.singlePhotoViewController!.loadPhoto();
}

}

func executePhoto(){
self.fbHelper!.fetchPhoto(self.albumModel.link);
}

func coverPhotoExecuted(notification:NSNotification){
let photos = notification.userInfo.valueForKey("photos") as UIImage[];
let backgroundImage = UIImageView(image: photos[0]);

self.tableView.backgroundView.addSubview(backgroundImage);
}

override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("photoExecuted:"), name: "photoNotification", object: nil);
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("coverPhotoExecuted:"), name: "coverPhotoNotification", object: nil);

self.navigationItem.title = self.albumModel.name;

super.viewDidLoad();
}

override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: "coverPhotoNotification", object: nil);
NSNotificationCenter.defaultCenter().removeObserver(self, name: "photoNotification", object: nil);
}

override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if(segue.identifier == "ShowEachSegue"){
self.singlePhotoViewController = segue.destinationViewController as? SinglePhotoViewController;
}
}

}

 

Assuming we’ve done everything properly so far, we should now have a main view where we can login and browse all the albums. Selecting each album should also show its constituent photos.

I plan to shed some light on “Functions and Closure” soon, as well as discussing “Nullable” type in Swift. Till then, keep your eyes peeled for the next tutorial!


Md. Arifuzzaman Md. Arifuzzaman is a Senior Software Engineer at SELISE rockin’ software. He has extensive experience in diverse facets of C#, .NET, BI development, and mobile app development (Objective-C, Android).