Send push notifications in a social network iOS app - Part 1: Build the backend
To follow this tutorial you will need a Mac with Xcode installed, and knowledge of Xcode and Swift. You'll also need basic knowledge of PHP (including the Laravel framework), a Pusher account, and Cocoapods installed on your machine.
Push notifications are a great way to engage users of your application. It lets you send notifications when certain events happen on your service. This can lead to re-engagement.
While building a social network app, you’ll need to send push notifications to your users. These notifications will help users know when certain events happen in your application. For instance, you can send push notifications to a user when someone comments on their photo.
As powerful as push notifications are, they are a double-edged sword. Most users will uninstall your application if they feel like they are being spammed.
Over the course of two articles, we will see how we can build a social networking iOS application. We will add push notifications to the user when someone comments on a photo they uploaded. Then we’ll add settings so users can specify when they want to receive notifications.
Prerequisites
To follow along in this tutorial you need to have the following:
- A Mac with Xcode installed.
- Knowledge of using Xcode.
- Knowledge of the Swift programming language.
- Knowledge of PHP and Laravel.
- Laravel CLI installed on your machine.
- SQLite installed on your machine. See installation guide.
- A Pusher beams API Key. Create one here.
- Cocoapods installed on your machine. See installation guide.
Creating our Pusher application
⚠️ To use push notifications, you have to be a part of the Apple Developer program. Also, push notifications do not work on simulators so you will need an actual iOS device to test.
Pusher Beams has first-class support for native iOS applications. Your iOS app instances subscribe to Interests; then your servers send push notifications to those interests. Every app instance subscribed to that interest will receive the notification, even if the app is not open on the device at the time.
This section describes how you can set up an iOS app to receive transactional push notifications about news updates through Pusher.
Configure APNs
Pusher relies on the Apple Push Notification service (APNs) to deliver push notifications to iOS application users on your behalf. When we deliver push notifications, we use your key that has APNs service enabled. This page guides you through the process of getting the key and how to provide it to Pusher.
Head over to the Apple Developer dashboard by clicking here and then create a new key as seen below:
When you have created the key, download it. Keep it safe as we will need it in the next section.
⚠️ You have to keep the generated key safe as you cannot get it back if you lose it.
Creating your Pusher application
The next thing you need to do is create a new Pusher Beams application from the Pusher dashboard.
When you have created the application, you should be presented with a quick start that will help you set up the application.
In order to configure your Beams instance, you will need to get the key with APNs service enabled by Apple. This is the same key as the one we downloaded in the previous section. Once you’ve got the key, upload it.
Enter your Apple Team ID. You can get the Team ID from here. You can then continue with the setup wizard and copy the instance ID and secret key for your Pusher application.
Building the backend
Before we start building the iOS application, let’s build the backend API using Laravel. To get started we need to set up our Laravel application. Run the command below using your terminal:
$ Laravel new gram
This will create a new Laravel application in the gram
directory.
Configuring our database
Our application will need to connect to a database and we will be using SQLite as our database of choice as it’s the easiest to set up.
To get started, create a new database.sqlite
file in the database
directory. Next open the .env
file that comes with the Laravel project and replace the following lines:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
With:
DB_CONNECTION=sqlite
DB_DATABASE=/full/path/to/database.sqlite
Now we have a connection to the database.
Creating our migrations, models, and controllers
When you want to create a migration, model, and controller, you should use the command below:
$ php artisan make:model ModelName -mc
Using the above command as a template, create the following models, migrations, and controllers:
Photo
PhotoComment
UserFollow
UserSetting
In that order.
After running the commands, we should have migrations in the database/migrations
directory, models in the app
directory, and controllers in the app/Http/Controllers
directory.
Let’s update the migrations. Open the *_create_photos_table.php
migration and replace the up
method with the following:
public function up()
{
Schema::create('photos', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
$table->string('image');
$table->string('image_path');
$table->string('caption')->nullable();
$table->timestamps();
});
}
Open the *_create_photo_comments_table.php
migration and replace the up
method with the following:
public function up()
{
Schema::create('photo_comments', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('photo_id');
$table->foreign('photo_id')->references('id')->on('photos');
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
$table->text('comment');
$table->timestamps();
});
}
Open the *_create_user_follows_table.php
migration and replace the up
method with the following:
public function up()
{
Schema::create('user_follows', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('follower_id');
$table->foreign('follower_id')->references('id')->on('users');
$table->unsignedInteger('following_id');
$table->foreign('following_id')->references('id')->on('users');
$table->timestamps();
});
}
Open the *_create_user_settings_table.php
migration and replace the up
method with the following:
public function up()
{
Schema::create('user_settings', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
$table->enum('notification_comments', ['Off', 'Following', 'Everyone'])
->default('Following');
});
}
That’s all for the migrations. Execute the migrations by running the command below:
$ php artisan migrate
When that’s done, we can update our models. Open the Photo
model in the app
directory and replace the contents with the following:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Photo extends Model
{
protected $hidden = ['image_path'];
protected $with = ['user', 'comments'];
protected $fillable = ['user_id', 'caption', 'image', 'image_path'];
public function user()
{
return $this->belongsTo(User::class);
}
public function comments()
{
return $this->hasMany(PhotoComment::class)->orderBy('id', 'desc');
}
}
In the model above we have the user
and comments
methods, which are relationships to the User
model and the PhotoComment
model.
Open the PhotoComment
class in the app
directory and replace the content with the following:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
class PhotoComment extends Model
{
use Notifiable;
protected $with = ['user'];
protected $fillable = ['photo_id', 'user_id', 'comment'];
protected $casts = ['photo_id' => 'int', 'user_id' => 'int'];
public function scopeForPhoto($query, int $id)
{
return $query->where('photo_id', $id);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
In the model above we are using the Notifiable
trait because we want to be able to send push notifications when new comments are made on photos later in the article. We also have a scopeForPhoto
method, which is an Eloquent query scope. We also have a user
method, which is a relationship to the User
model.
Open the User
model in the app
directory and replace the content with the following:
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
public function allowsCommentsNotifications(User $actor)
{
$status = strtolower($this->settings->notification_comments);
switch ($status) {
case 'everyone': return true;
case 'following': return $this->isFollowing($actor);
default: return false;
}
}
public function isFollowing(User $user): bool
{
return $this->following->where('following_id', $user->id)->count() > 0;
}
public function scopeOtherUsers($query)
{
return $query->where('id', '!=', auth()->user()->id);
}
public function following()
{
return $this->hasMany(UserFollow::class, 'follower_id');
}
public function followers()
{
return $this->hasMany(UserFollow::class, 'following_id');
}
public function settings()
{
return $this->hasOne(UserSetting::class);
}
}
In the model above we have six methods:
allowsCommentsNotifications
checks to see if the owner of the photo has settings that permit notifications to be sent to them when there is a new comment.isFollowing
checks if a user is following another user.scopeOtherUsers
is an Eloquent query scope.following
,followers
andsettings
are methods that define relationships with other models.
Open the UserFollow
model in the app
directory and replace the content with the following:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class UserFollow extends Model
{
protected $fillable = ['follower_id', 'following_id'];
}
Finally, open the UserSetting
model in the app
directory and replace the content with the following:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class UserSetting extends Model
{
protected $fillable = ['notification_comments'];
protected $hidden = ['id', 'user_id'];
public $timestamps = false;
public function scopeForCurrentUser($query)
{
return $query->where('user_id', auth()->user()->id);
}
}
Above we have the scopeForCurrentUser
method, which is an Eloquent query scope.
We set the
$timestamps
property to false to instruct Eloquent not to attempt to automatically manage thecreated_at
andupdated_at
fields as we do not have them in the user settings table.
One last thing we want to do is, create a new setting automatically when a user is created. For this, we will use an Eloquent event. Open the AppServiceProvider
class in the app/Providers
directory and replace the boot method with the following:
public function boot()
{
\App\User::created(function ($user) {
$user->settings()->save(new \App\UserSetting);
});
}
As seen above, when a new user is created, a new user setting is saved to the user.
Next, let’s update the logic for the controllers. Open the PhotoController.php
in the app/Http/Controllers
directory and replace the contents with the following:
<?php
namespace App\Http\Controllers;
use App\Photo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class PhotoController extends Controller
{
public function index()
{
$photos = Photo::orderBy('id', 'desc')->paginate(20);
return response()->json($photos->toArray());
}
public function store(Request $request)
{
$data = $request->validate([
'caption' => 'required|between:1,1000',
'image' => 'required|image|mimes:jpeg,gif,png',
]);
$path = Storage::disk('public')->putFile('photos', $request->file('image'));
$data = array_merge($data, [
'user_id' => $request->user()->id,
'image' => asset("storage/{$path}"),
'image_path' => storage_path('app/public') . "/{$path}",
]);
$photo = Photo::create($data);
return response()->json([
'status' => 'success',
'data' => $photo->load(['user', 'comments'])
]);
}
}
In the PhotoController
above we have two methods. The index
displays all the available photos, and the store
saves a new photo to disk and database.
For the photos
saved to be available to the public, we need to link the storage
directory to the public directory. To do so run the command below:
$ php artisan storage:link
The command above will create a symlink from the public/storage
directory to the storage/app/public
directory that our photos will be uploaded to.
Open the PhotoCommentController.php
in the app/Http/Controllers
directory and replace the contents with the following:
<?php
namespace App\Http\Controllers;
use App\Photo;
use App\PhotoComment;
use Illuminate\Http\Request;
use App\Notifications\UserCommented;
class PhotoCommentController extends Controller
{
public function index(Request $request)
{
$photo = Photo::with('comments')->findOrFail($request->route('photo'));
return response()->json(['data' => $photo->comments]);
}
public function store(Request $request, Photo $photo)
{
$data = $request->validate(['comment' => 'required|string|between:2,500']);
$comment = PhotoComment::create([
'photo_id' => $photo->id,
'comment' => $data['comment'],
'user_id' => $request->user()->id,
]);
if ($photo->user->allowsCommentsNotifications($request->user())) {
$comment->notify(new UserCommented($request->user(), $photo, $comment));
}
return response()->json([
'status' => 'success',
'data' => $comment->load('user')
]);
}
}
In the PhotoCommentController
we have two methods. The index
method displays all the comments for a single photo and the store
creates a new comment.
In the store
method on line 30, we have a call to a notify
method and passes a nonexistent UserCommented
class. This class is a Laravel notification class. We will create this class later in the article. It’s needed to send notifications to the user when comments are made.
Create a UserController
by running the command below:
$ php artisan make:controller UserController
Next open the UserController.php
in the app/Http/Controllers
directory and replace the contents with the following:
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
public function index()
{
$users = [];
User::with('followers')->otherUsers()->get()->each(function ($user, $index) use (&$users) {
$users[$index] = $user;
$users[$index]['follows'] = auth()->user()->isFollowing($user);
});
return response()->json(['data' => $users]);
}
public function create(Request $request)
{
$credentials = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6',
]);
$credentials['password'] = Hash::make($credentials['password']);
$user = User::create($credentials);
$token = $user->createToken(config('app.name'));
$data = ['user' => $user, 'access_token' => $token->accessToken];
return response()->json(['data' => $data, 'status' => 'success']);
}
}
The UserController
has two methods, one is the index
method that returns all the users on the service, and the second is the create
method that registers a new user and returns an access token that will be used for making authorized requests on behalf of the user.
Open the UserFollowController.php
in the app/Http/Controllers
directory and replace the contents with the following:
<?php
namespace App\Http\Controllers;
use App\User;
use App\UserFollow;
use Illuminate\Http\Request;
class UserFollowController extends Controller
{
public function follow(Request $request)
{
$user = User::findOrFail($request->get('following_id'));
if ($request->user()->isFollowing($user) == false) {
$request->user()->following()->save(
new UserFollow($request->only('following_id')
));
}
return response()->json(['status' => 'success']);
}
public function unfollow(Request $request)
{
$user = User::findOrFail($request->get('following_id'));
$request->user()->following()->whereFollowingId($user->id)->delete();
return response()->json(['status' => 'success']);
}
}
The controller above simply follows or unfollows a user.
Open the UserSettingController.php
in the app/Http/Controllers
directory and replace the contents with the following:
<?php
namespace App\Http\Controllers;
use App\UserSetting;
use Illuminate\Http\Request;
class UserSettingController extends Controller
{
public function index()
{
return response()->json(UserSetting::forCurrentUser()->first());
}
public function update(Request $request)
{
$settings = $request->validate([
'notification_comments' => 'in:Off,Following,Everyone',
]);
$updated = $request->user()->settings()->update($settings);
return response()->json(['status' => $updated ? 'success' : 'error']);
}
}
In the controller above we return all the settings available for a user in the index
method and then we update the settings for the user in the update
method.
Creating our application’s routes
Since we have created our controllers, let’s create our routes that link the URL to controllers. Open the routes/api.php
file and replace the contents with the following:
<?php
Route::post('/register', 'UserController@create');
Route::group(['middleware' => 'auth:api'], function () {
Route::get('/users/settings', 'UserSettingController@index');
Route::put('/users/settings', 'UserSettingController@update');
Route::post('/users/follow', 'UserFollowController@follow');
Route::post('/users/unfollow', 'UserFollowController@unfollow');
Route::get('/users', 'UserController@index');
Route::get('/photos/{photo}/comments', 'PhotoCommentController@index');
Route::post('/photos/{photo}/comments', 'PhotoCommentController@store');
Route::resource('/photos', 'PhotoController')->only(['store', 'index']);
});
Above we have defined routes for our application. Each route points to a controller and a method in that controller that will handle the route. The route group above has a middleware applied, auth:api
, this will make sure that every request to a route inside the group has to be authorized.
To manage authorization, let’s install Laravel passport.
Installing Laravel Passport
Since we have many requests that require authorization, let’s install Laravel Passport. In the root directory of your project and run the following command:
$ composer require laravel/passport
This will install Laravel Passport to the project. Open the User
model in the app
directory and use
the HasApiTokens
trait:
<?php
// [...]
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
// [...]
}
Next open the AuthServiceProvider
class in the app/Providers
directory and update it to the following:
<?php
// [...]
use Laravel\Passport\Passport;
class AuthServiceProvider extends ServiceProvider
{
// [...]
public function boot()
{
// [...]
Passport::routes();
}
}
Open the config/auth.php
configuration file and set the driver
option of the api
authentication guard to passport
. This will instruct your application to use Passport’s TokenGuard
when authenticating incoming API requests:
'guards' => [
// [...]
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
To complete the installation, run the commands below, which will perform a migration and install Laravel Passport to your application:
$ php artisan migrate
$ php artisan passport:install
Passport is successfully installed after the commands finish execution. The passport:install
command will create two files in the storage
directory: oauth-public.key
and oauth-private.key
. These keys will be used to sign and validate access tokens.
⚠️ Copy and save the client ID and secret for the second client as you will need it later in the article.
Adding push notification support
The next thing we want to do is add push notification support. For this, we will be using Pusher Beams. For convenience, we will be using a PHP library that is a Laravel supported wrapper for the Pusher Beams PHP library.
In your terminal run the following command:
$ composer require neo/pusher-beams
When the installation is completed, open the .env
file and add the following keys to the file:
PUSHER_BEAMS_SECRET_KEY="PUSHER_BEAMS_SECRET_KEY"
PUSHER_BEAMS_INSTANCE_ID="PUSHER_BEAMS_INSTANCE_ID"
💡 You need to replace the
PUSHER_BEAMS_SECRET_KEY
andPUSHER_BEAMS_INSTANCE_ID
keys with the keys gotten from your Pusher dashboard.
Open the broadcasting.php
file in the config
directory and add the following keys to the pusher connection array:
'connections' => [
'pusher' => [
// [...]
'beams' => [
'secret_key' => env('PUSHER_BEAMS_SECRET_KEY'),
'instance_id' => env('PUSHER_BEAMS_INSTANCE_ID'),
],
// [...]
],
],
Next, create a new notification class where we will add our push notification. In your terminal run the command below to create the class:
$ php artisan make:notification UserCommented
This will create a new UserCommented
class in the app/Notifications
directory. Open the file and replace the contents with the following:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Neo\PusherBeams\PusherBeams;
use Neo\PusherBeams\PusherMessage;
use App\User;
use App\PhotoComment;
use App\Photo;
class UserCommented extends Notification
{
use Queueable;
public $user;
public $comment;
public $photo;
public function __construct(User $user, Photo $photo, PhotoComment $comment)
{
$this->user = $user;
$this->photo = $photo;
$this->comment = $comment;
}
public function via($notifiable)
{
return [PusherBeams::class];
}
public function toPushNotification($notifiable)
{
return PusherMessage::create()
->iOS()
->sound('success')
->title('New Comment')
->body("{$this->user->name} commented on your photo: {$this->comment->comment}")
->setOption('apns.aps.mutable-content', 1)
->setOption('apns.data.attachment-url', $this->photo->image);
}
public function pushNotificationInterest()
{
$id = $this->photo->id;
$audience = strtolower($this->user->settings->notification_comments);
return "photo_{$id}-comment_{$audience}";
}
}
In the class above we are extending a Notification
class and we have implemented the toPushNotification
method, which will be used to send the push notification when required. In the via
method, we specify what channels we want to send the notification through and in the pushNotificationInterest
we specify the interest we want to publish the push notification to.
If you remember earlier, we invoked the notification on line 30 of the PhotoCommentController
.
💡 Read more about Laravel Notifications and how it works.
That’s it. The backend application is complete. To start serving the application, run the following command:
$ php artisan serve
This will start a PHP server running on port 8000.
Building our iOS application using Swift
Now that we have a backend server that can serve us all the information we want and also send push notifications, let us create our iOS application, which will be the client application.
Launch Xcode and create a new ‘Single Page App’ project. Let’s call it Gram. When the project is created, exit Xcode and cd
to the root of the project using a terminal. In the root of the project create a Podfile
and paste the following into the file:
platform :ios, '11.0'
target 'Gram' do
use_frameworks!
pod 'Alamofire', '~> 4.7.1'
pod 'PushNotifications', '~> 0.10.6'
pod 'NotificationBannerSwift'
end
Then run the command below to start installing the dependencies we defined above:
$ pod install
When the installation is complete, we will have a new .xcworkspace
file in the root of the project. Double-click the workspace file to relaunch Xcode.
Creating our storyboard
Next, let’s create our storyboard. Open your Main.storyboard
file. We want to design it to look similar to this:
How the storyboard scenes are connected
The first scene we have a launch view controller. This controller connects to the login scene, register scene or the main navigation controller depending on the login status of the user. The login and register scenes are basic and they simply authenticate the user.
The main navigation controller connects to the main controller that displays the timeline. From that scene, there are connections to the settings scene, the search scene, and the view comments scene. Each segue connection is given an identifier so we can present them from the controller code.
When you are done creating the storyboard, let’s create the custom classes for each storyboard scene.
Creating our models
To help us with managing our API’s JSON responses we will be using Codable in Swift 4. This will make it extremely easy for us to manage the responses from the API.
Create a new file named Models.swift
and paste this in the file:
import Foundation
typealias Users = [User]
typealias Photos = [Photo]
typealias PhotoComments = [PhotoComment]
struct User: Codable {
var id: Int
var name: String
var email: String
var follows: Bool?
}
struct Photo: Codable {
var id: Int
var user: User
var image: String
var caption: String
var comments: PhotoComments
}
struct PhotoComment: Codable {
var id: Int
var user: User
var photo_id: Int
var user_id: Int
var comment: String
}
Creating our services
Our services will contain code that we will need to make calls to the API and also other functionality that interacts with the application view.
Create a new class SettingsService
and paste the following code into the file:
import Foundation
class SettingsService: NSObject {
static let shared = SettingsService()
static let key = "gram.settings.notifications"
var settings: [String: String] = [:];
private var allSettings: [String: String] {
set {
self.settings = newValue
}
get {
if let settings = loadFromDefaults(), settings["notification_comments"] != nil {
return settings
}
return [
"notification_comments": Setting.Notification.Comments.following.toString()
];
}
}
override private init() {
super.init()
self.settings = self.allSettings
}
func loadFromDefaults() -> [String: String]? {
return UserDefaults.standard.dictionary(forKey: SettingsService.key) as? [String: String]
}
func loadFromApi() {
ApiService.shared.loadSettings { settings in
if let settings = settings {
self.allSettings = settings
self.saveSettings(saveRemotely: false)
}
}
}
func updateCommentsNotificationSetting(_ status: Setting.Notification.Comments) {
self.allSettings["notification_comments"] = status.toString()
saveSettings()
}
func saveSettings(saveRemotely: Bool = true) {
UserDefaults.standard.set(settings, forKey: SettingsService.key)
if saveRemotely == true {
ApiService.shared.saveSettings(settings: settings) { _ in }
}
}
}
In the class above we have defined the settings service. The class is how we manage settings for our application. In the allSettings
setter, we attempt to fetch the settings from the local store and if we cant, we return some sensible defaults.
We have the loadFromDefaults
method that loads the settings locally from the UserDefaults
, the loadFromApi
class that loads settings from the API using the ApiService
, the updateCommentsNotificationSetting
, which updates the comment notification settings. Finally, we have the saveSettings
method that saves the comment locally and remotely.
In the same file, add the following enum
to the bottom:
enum Setting {
enum Notification {
enum Comments: String {
case off = "Off"
case everyone = "Everyone"
case following = "Following"
func toString() -> String {
return self.rawValue
}
}
}
}
The enum is basically a representation of the available settings for our comment notifications.
The next service we want to define is the AuthService
. This service is used to authenticate users of our service. Create a new AuthService
class and paste the following code into it:
import Foundation
class AuthService: NSObject {
static let key = "gram-token"
static let shared = AuthService()
typealias AccessToken = String
typealias LoginCredentials = (email: String, password: String)
typealias SignupCredentials = (name: String, email: String, password: String)
override private init() {
super.init()
}
func loggedIn() -> Bool {
return getToken() != nil
}
func logout() {
UserDefaults.standard.removeObject(forKey: AuthService.key)
}
func getToken() -> AccessToken? {
return UserDefaults.standard.string(forKey: AuthService.key)
}
func saveToken(_ token: AccessToken) -> AuthService {
UserDefaults.standard.set(token, forKey: AuthService.key)
return self
}
func deleteToken() -> AuthService {
UserDefaults.standard.removeObject(forKey: AuthService.key)
return self
}
func then(completion: @escaping() -> Void) {
completion()
}
}
The class above is fairly straightforward and it provides methods for authentication. It has the getToken
and saveToken
, which essentially retrieves and saves the access token gotten after authenticating the user.
Next, let’s create our final service, the ApiService
. Create a new class ApiService
and paste the following into the file:
import Foundation
import Alamofire
class ApiService: NSObject {
static let shared = ApiService()
override private init() {
super.init()
}
}
Now that we have the base of the class, let’s start adding methods to the class. Since it is a large class, we will split adding the methods over a few paragraphs.
In the class, let’s add our first set of methods, which will handle authentication:
func login(credentials: AuthService.LoginCredentials, completion: @escaping(AuthService.AccessToken?, ApiError?) -> Void) {
let params = [
"username": credentials.email,
"password": credentials.password,
"grant_type": "password",
"client_id": AppConstants.API_CLIENT_ID,
"client_secret": AppConstants.API_CLIENT_SECRET
]
request(.post, url: "/oauth/token", params: params, auth: false) { data in
guard let data = data else { return completion(nil, .badCredentials) }
guard let token = data["access_token"] as? String else { return completion(nil, .badResponse) }
completion(token, nil)
}
}
func signup(credentials: AuthService.SignupCredentials, completion: @escaping(AuthService.AccessToken?, ApiError?) -> Void) {
let params = [
"name": credentials.name,
"email": credentials.email,
"password": credentials.password
]
request(.post, url: "/api/register", params: params, auth: false) { data in
guard let res = data, let data = res["data"] as? [String:AnyObject] else {
return completion(nil, .badCredentials)
}
guard let token = data["access_token"] as? String else {
return completion(nil, .badResponse)
}
completion(token, nil)
}
}
Next let’s add the methods for loading users, loading posts, loading comments and adding comments to the ApiService
class:
func fetchUsers(completion: @escaping(Users?) -> Void) {
request(.get, url: "/api/users") { data in
if let data = self.responseToJsonStringData(response: data) {
if let obj = try? JSONDecoder().decode(Users.self, from: data) {
return completion(obj)
}
}
completion(nil)
}
}
func fetchPosts(completion: @escaping(Photos?) -> Void) {
request(.get, url: "/api/photos") { data in
if let data = self.responseToJsonStringData(response: data) {
if let obj = try? JSONDecoder().decode(Photos.self, from: data) {
return completion(obj)
}
}
completion(nil)
}
}
func fetchComments(forPhoto id: Int, completion: @escaping(PhotoComments?) -> Void) {
request(.get, url: "/api/photos/\(id)/comments") { data in
if let data = self.responseToJsonStringData(response: data) {
if let obj = try? JSONDecoder().decode(PhotoComments.self, from: data) {
return completion(obj)
}
}
completion(nil)
}
}
func leaveComment(forId id: Int, comment: String, completion: @escaping(PhotoComment?) -> Void) {
request(.post, url: "/api/photos/\(id)/comments", params: ["comment": comment]) { data in
if let res = data, let data = res["data"] as? [String: AnyObject],
let json = try? JSONSerialization.data(withJSONObject: data, options: []),
let jsonString = String(data: json, encoding: .utf8),
let jsonData = jsonString.data(using: .utf8),
let obj = try? JSONDecoder().decode(PhotoComment.self, from: jsonData) {
return completion(obj)
}
completion(nil)
}
}
In the methods above, you’ll notice we decode the JSON response from the API into the appropriate model object. This makes it easier to work with in our controllers.
The next methods we will add will be to follow or unfollow a user, load settings for a user and update settings for a user. Add the methods below to the ApiService
:
func toggleFollowStatus(forUserId id: Int, following: Bool, completion: @escaping(Bool?) -> Void) {
let status = following ? "unfollow" : "follow"
request(.post, url: "/api/users/\((status))", params: ["following_id": id]) { data in
guard let res = data as? [String: String], res["status"] == "success" else {
return completion(false)
}
completion(true)
}
}
func loadSettings(completion: @escaping([String: String]?) -> Void) {
request(.get, url: "/api/users/settings") { data in
guard let settings = data as? [String: String] else {
return completion(nil)
}
completion(settings)
}
}
func saveSettings(settings: [String: String], completion: @escaping(Bool) -> Void) {
request(.put, url: "/api/users/settings", params: settings) { data in
guard let res = data as? [String: String], res["status"] == "success" else {
return completion(false)
}
completion(true)
}
}
The next method we want to add is the uploadImage
method. This method is responsible for taking the selected image and caption and sending it to the API for uploading. Add the method below to the ApiService
class:
func uploadImage(_ image: Data, caption: String, name: String, completion: @escaping(Photo?, ApiError?) -> Void) {
let url = self.url(appending: "/api/photos")
// Handles multipart data
let multipartHandler: (MultipartFormData) -> Void = { multipartFormData in
multipartFormData.append(caption.data(using: .utf8)!, withName: "caption")
multipartFormData.append(image, withName: "image", fileName: name, mimeType: "image/jpeg")
}
Alamofire.upload(
multipartFormData: multipartHandler,
usingThreshold: UInt64.init(),
to: url,
method: .post,
headers: requestHeaders(),
encodingCompletion: { encodingResult in
let uploadedHandler: (DataResponse<Any>) -> Void = { response in
if response.result.isSuccess,
let resp = response.result.value as? [String: AnyObject],
let data = resp["data"] as? [String: AnyObject],
let json = try? JSONSerialization.data(withJSONObject: data, options: []),
let jsonString = String(data: json, encoding: .utf8),
let jsonData = jsonString.data(using: .utf8),
let obj = try? JSONDecoder().decode(Photo.self, from: jsonData) {
return completion(obj, nil)
}
completion(nil, .uploadError(nil))
}
switch encodingResult {
case .failure(_): completion(nil, .uploadError(nil))
case .success(let upload, _, _): upload.responseJSON(completionHandler: uploadedHandler)
}
}
)
}
Next let’s add the class’ helper methods.
private func url(appending: URLConvertible) -> URLConvertible {
return "\(AppConstants.API_URL)\(appending)"
}
private func requestHeaders(auth: Bool = true) -> HTTPHeaders {
var headers: HTTPHeaders = ["Accept": "application/json"]
if auth && AuthService.shared.loggedIn() {
headers["Authorization"] = "Bearer \(AuthService.shared.getToken()!)"
}
return headers
}
private func request(_ method: HTTPMethod, url: URLConvertible, params: Parameters? = nil, auth: Bool = true, handler: @escaping ([String: AnyObject]?) -> Void) {
let url = self.url(appending: url)
Alamofire
.request(url, method: method, parameters: params, encoding: JSONEncoding.default, headers: requestHeaders(auth: auth))
.validate()
.responseJSON { resp in
guard resp.result.isSuccess, let data = resp.result.value as? [String: AnyObject] else {
return handler(nil)
}
handler(data)
}
}
func responseToJsonStringData(response data: [String: AnyObject]?) -> Data? {
if let res = data, let data = res["data"] as? [[String: AnyObject]] {
if let json = try? JSONSerialization.data(withJSONObject: data, options: []) {
if let jsonString = String(data: json, encoding: .utf8), let data = jsonString.data(using: .utf8) {
return data
}
}
}
return nil
}
The url
method takes a URL path and appends the base API URL to it. The requestHeaders
method attaches the appropriate headers to the request sent by Alamofire. The request
method is a wrapper around Alamofire that sends requests to the API for us. The responseToJsonStringData
converts the data from our JSON file into a JSON string which can then be decoded into one of our Codable
models.
One final thing we want to add to the bottom of the ApiService
class is the enum
for ApiError
s. In the same file at the bottom, add the following code:
enum ApiError: Error {
case badResponse
case badCredentials
case uploadError([String: [String]]?)
}
That’s all for the ApiService
and indeed all the applications services. In the next part we will continue building our iOS application.
Conclusion
In this first part of the article, we have seen how we can create an API for our social network application using Laravel. We also integrated push notifications on the server side using Pusher Beams.
In the next part, we will build the client IOS application using Swift. We will also integrate push notifications to our social network application using Pusher Beams.
The source code to the application is on GitHub.
25 May 2018
by Neo Ighodaro