Overview

Full-stack Swift – Part 2

No Comments

In the first part of this blog post, we have successfully built a simple Vapor web server.
Now we are going to complement it with an iOS app.

The app will be written in Swift and it will be using Google’s Protocol Buffers (from now on protobuf) for communicating with the server over HTTP.

The app should be able to:

  • Display the existing books that are stored on the server
  • Add new books to the server

Building the UI of the app is outside the scope of this blog post, instead we will focus on the network layer and working with protobuf.

PROJECT SETUP

Create a new Xcode Single View Application project.
Name it BookshelfApp.
Add the previously generated Bookshelf.pb.swift into your project in order to be able to work with Book and Shelf objects.
In order to be able to use this class, we need to add Apple’s protobuf runtime library to the project.

We will use Carthage for dependency management. If you don’t already have it installed, install it using the official instructions.

Create a Cartfile that will be used by Carthage for fetching dependencies:

$ echo 'github "apple/swift-protobuf"' >> Cartfile

Fetch dependencies listed in your Cartfile into a Carthage/Checkouts folder, and build each one of them by running:

$ carthage update --platform iOS

In your application’s target “General” settings tab, in the “Linked Frameworks and Libraries” section, drag and drop SwiftProtobuf.framework from the Carthage/Build folder.

In your application’s target “Build Phases” settings tab, click the “+” icon and choose “New Run Script Phase”. Create a Run Script in which you specify your shell (ex: bin/sh), add the following contents to the script area below the shell:

$ /usr/local/bin/carthage copy-frameworks

and add the path to the SwiftProtobuf.framework under “Input Files”:
$(SRCROOT)/Carthage/Build/iOS/SwiftProtobuf.framework

Now we can use parse bytes into and from Book and Shelf objects.

NETWORK LAYER

Although many iOS Developers prefer using libraries such as AFNetworking (ObjC) / Alamofire (Swift) for creating their network layer, but in my opinion you should minimize the number of dependencies whenever you can.
In the case of iOS and networking, Apple has done a really god job with the URLSession class, and I will be using it for the core of the networking layer.

Create an empty Swift file called BackendRequest.
In this file we will define a protocol that every backend request will need to implement:

protocol BackendRequest {
    var endpoint: String { get }
    var method: BackendRequestMethod { get }
    var headers: [String: String]? { get }
    var data: Data? { get }
    func didSucceed(with data: Data)
    func didFail(with error: Error)
    func execute()
}

Each request to the backend needs to have:

  • An endpoint that it’s targeting
  • HTTP method
  • HTTP headers
  • HTTP body data
  • A way to handle data it got from the server
  • A way to handle an error
  • Method to enable executing it

As you can see we are missing the BackendRequestMethod type.
It will be an enum with a raw value of String, define it just above the protocol definition:

enum BackendRequestMethod: String {
    case get, post, put, delete
}

Also, let’s define some HTTP headers that we will use for our request just above the protocol definition:

struct BackendRequestHeader {
    static let contentTypeOctetStream = ["Content-Type": "application/octet-stream"]
    static let acceptOctetStream = ["Accept": "application/octet-stream"]
}

Add an extension with a default implementation of the execute method that will execute the request on the shared instance (singleton) of the BackendService (not implemented yet), below the protocol :

extension BackendRequest { 
    func execute() {
        BackendService.sharedInstance.execute(backendRequest: self)
    }
}

Finally, define typealiases for default request completion handling above the protocol definition:

typealias SuccessHandler = (Void) -> Void
typealias FailureHandler = (Error) -> Void

As you can see, we haven’t yet implemented the BackendService class that is capable of executing our BackendRequests, so we need to create it.
Create a new Swift file named BackendService.
Inside it define a new class called BackendService:

final class BackendService {
    public static let sharedInstance = BackendService()
    let baseUrl = URL(string: "http://localhost:8080")!
    let session = URLSession.shared
}

BackendService is going to be a singleton.
We are going to keep it as simple as possible. In a real project it could be improved by adding a priority queue, and some configuration options.
We added a baseUrl for our local instance of web server, and we added a shared URLSession.
Time to add the core method for executing backend requests. Inside the BackendService class, add the execute method:

public func execute(backendRequest: BackendRequest) {
 
    var urlRequest = URLRequest(url: baseUrl.appendingPathComponent(backendRequest.endpoint), cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 10.0)
 
    urlRequest.httpMethod = backendRequest.method.rawValue
 
    if let data = backendRequest.data {
        urlRequest.httpBody = data
    }
 
    if let headers = backendRequest.headers {
        for (key, value) in headers {
            urlRequest.addValue(value, forHTTPHeaderField: key)
        }
    }
 
    let task = session.dataTask(with: urlRequest) { (data, response, error) in
        guard let data = data, let _ = response, error == nil else {
            if let error = error {
                backendRequest.didFail(with: error)
            }
            return
        }
        backendRequest.didSucceed(with: data)
    }
 
    task.resume()
}

In this method we are:

  1. Creating a new URLRequest, and setting it’s url by combining the baseUrl and the BackendRequest’s endpoint
  2. Setting the URLRequest http method
  3. Checking the backendRequest for serialized data, and if there is any filling the URLRequest’s httpBody with it
  4. Going through the BackendRequest’s headers and setting the URLRequest’s headers appropriately
  5. Setting the session dataTask callback to properly return data or an error by using BackendRequest’s methods for it
  6. Starting the URLSession’s data task by calling the resume method on the task

In order to see if our BackendService is working properly, let’s try fetching data from our server.
We need to implement a proper BackendRequest first.
Create a new Swift file named BackendGetShelfRequest.
Inside it, create a new struct named BackendGetShelfRequest, and a typealias for this request’s completion handler:

typealias GetShelfSuccessHandler = (Shelf) -> Void
 
struct BackendGetShelfRequest {
    var successHandler: GetShelfSuccessHandler?
    var failureHandler: FailureHandler?
}

Implement the BackendRequest protocol in an extension:

extension BackendGetShelfRequest: BackendRequest {
    var endpoint: String {
        return "shelf"
    }
 
    var method: BackendRequestMethod {
        return .get
    }
 
    var headers: [String: String]? {
        return BackendRequestHeader.acceptOctetStream
    }
 
    var data: Data? {
        return nil
    }
 
    func didSucceed(with data: Data) {
        if let shelf = try? Shelf(protobuf: data), let successHandler = successHandler {
            successHandler(shelf)
        }
    }
 
    func didFail(with error: Error) {
        if let failureHandler = failureHandler {
            failureHandler(error)
        }
    }
}

The crucial part is the didSucceed method.
We are getting a serialized protobuf from the server and we are trying to deserialize it into an Shelf object.
A lot of edge cases are not covered.
In a real project you should check the response headers and do some more checks and error handling, but that is outside of the scope of this blog post.
If deserialization is successful and there is a successHandler, we pass the resulting shelf object to the completion handler.
In a proper app, we would save the results of our request in a persistence layer such as CoreData, but here we are just passing the results to the completion handler to be used in the app from memory.

All we need to do now is run our previously created server and then, in our app, init and execute an instance of BackendGetShelfRequest:

var request = BackendGetShelfRequest()
 
request.successHandler = { shelf in
    DispatchQueue.main.async {
        for book in shelf.books {
            print(book)
        }
    }
}
 
request.failureHandler = { error in
    DispatchQueue.main.async {
        print(error.localizedDescription)
    }
}
 
request.execute()

Try this out in your ViewController’s viewDidLoad method, and if you have done everything correctly you will see an error in your console.
Because of iOS inbuilt security you can not send a non-https request without downgrading the security settings of your app.
Inside your Info.plist add:

<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
	</dict>

This should not be done in a real app, and you should use https for your web server.

Try running your app again, and now you will see the data of your books on server printed out.

Adding a book is quite similar with a few differences:

struct BackendAddBookRequest {
 
    let book: Book
    var successHandler: SuccessHandler?
    var failureHandler: FailureHandler?
 
    init(book: Book, successHandler: SuccessHandler? = nil, failureHandler: FailureHandler? = nil) {
        self.book = book
        self.successHandler = successHandler
        self.failureHandler = failureHandler
    }
}
 
extension BackendAddBookRequest: BackendRequest {
 
    var endpoint: String {
        return "book"
    }
 
    var method: BackendRequestMethod {
        return .post
    }
 
    var headers: [String: String]? {
        return BackendRequestHeader.contentTypeOctetStream
    }
 
    var data: Data? {
        guard let serializedBook = try? book.serializeProtobuf() else {
            return nil
        }
        return serializedBook
    }
 
    func didSucceed(with data: Data) {
        if let successHandler = successHandler {
            successHandler()
        }
    }
 
    func didFail(with error: Error) {
        if let failureHandler = failureHandler {
            failureHandler(error)
        }
    }
}

We init a BackendAddBookRequest a Book object.
In the data computed variable we try to serialize the book into protobuf bytes.
If the serialization is successful we return the serialized protobuf.
If it fails we return nil.
The rest is handled by our implementation of BackendService.

The rest of the project is basic iOS table-based UI for displaying and adding books, which I belive most of you reading this are acquainted with, and building that part is outside of the scope of this blog post.
If you want to take this project further, you can take this from here and implement proper controllers inside the Vapor app, add/or add a persistence layer on both server and client side.

WRAPPING UP

In the first part of this blog post we created a simple web server using Vapor web framework and Swift programming language.
We demonstrated Swift’s simplicity and expressiveness and it’s huge potential for server-side development.
In this part of the blog post, we created the complementary iOS app that communicates with previously created web server.
In both the client and server-side code we used Protocol Buffers for serializing and deserializing data in our HTTP requests.

Full source code is available at the project’s github repository.

I hope you have enjoyed this blog post, feel free to comment and ask any questions you want!

Milan Stevanovic

Milan Stevanovic is working for codecentric Serbia as an IT Consultant and is mainly involved in iOS app consulting and development.

You can follow him on Twitter.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Comment

Your email address will not be published. Required fields are marked *