Date Archives

September 2018

Swift: An app to search through movie titles using The Open Movie Database API

This demo app uses OMDb APIs to search in movie titles and show details of the selected movie. First of all, you need to get your API key, it’s free. All right, this is the plan:

  • Create a class to make network requests usingĀ URLSession
  • Test the class
  • Create the UI

NetworService will handle the network requests. This is a singleton class which uses the builtin URLSession with a handful of configuration options. An extension to this class contains the required methods to search. Since I have created this as a reusable utility class, there are some extra features which won’t be used in this tutorial, maybe you need them later.

class NetworkService {
    //MARK: - Internal structs
    private struct authParameters {
        struct Keys {
            static let accept = "Accept"
            static let apiKey = "apikey"
        }
        
        static let apiKey = "YOURKEY"
    }
    
    //An NSCach object to cache images, if necessary
    private let cache = NSCache<NSString, NSData>()
    
    //Default session configuration
    private let urlSessionConfig = URLSessionConfiguration.default
    
    //Additional headers such as authentication token, go here
    private func configSession(){
        self.urlSessionConfig.httpAdditionalHeaders = [
            AnyHashable(authParameters.Keys.accept): MIMETypes.json.rawValue
        ]
    }
    
    private static var sharedInstance: NetworkService = {
        return NetworkService()
    }()
    
    //MARK: - Public APIs
    class func shared() -> NetworkService {
        sharedInstance.configSession()
        return sharedInstance
    }

    //MARK: - Private APIs
    private func createAuthParameters(with parameters:[String:String]) -> Data? {
        guard parameters.count > 0 else {return nil}
        return  parameters.map {"\($0.key)=\($0.value)"}.joined(separator: "&amp;").data(using: .utf8)
    }
}

This is the skeleton of our class, shared() function returns a static instance of the class after running the internalĀ configSession() function. authParameters structure is used to store keys and values for authentication, just to prevent writing a messy code. Now we can create an instance of the class using, let networkService = NetworkService.shared().

Now the we need another public method to make network requests:

    private func request(url:String,
                 cachePolicy: URLRequest.CachePolicy = .reloadRevalidatingCacheData,
                 httpMethod: RequestType,
                 headers:[String:String]?,
                 body: [String:String]?,
                 parameters: [URLQueryItem]?,
                 useSharedSession: Bool = false,
                 handler: @escaping (Data?, URLResponse?, Int?, Error?) -> Void){
        
        if var urlComponent = URLComponents(string: url) {
            urlComponent.queryItems = parameters
            var session = URLSession(configuration: urlSessionConfig)
            if useSharedSession {
                session = URLSession.shared
            }
            
            if let _url = urlComponent.url {
                
                var request = URLRequest(url: _url)
                request.cachePolicy = cachePolicy
                request.allHTTPHeaderFields = headers
                
                if let _body = body {
                    request.httpBody = createAuthParameters(with: _body)
                }
                request.httpMethod = httpMethod.rawValue
                
                session.dataTask(with: request) { (data, response, error) in
                    let httpResponsStatusCode = (response as? HTTPURLResponse)?.statusCode
                    handler(data, response, httpResponsStatusCode, error)
                    }.resume()
            }else{
                handler(nil, nil, nil, Failure.invalidURL)
            }
        }else{
            handler(nil, nil, nil, Failure.invalidURL)
        }
    }

This method has the basic functionality of a request session, getting parameters and headers and returning response, data, status code and an error object if exists, asynchronously. For the httpPart, you can replaceĀ createAuthParameters(:_) with an array of URLQueryItems.

For errors, I’m using a enumerator which is inherited from Error protocol, here is it:

import Foundation
public enum Failure:Error {
    case invalidURL
    case invalidSearchParameters
    case invalidResults(String)
    case invalidStatusCode(Int?)
}

extension Failure: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .invalidURL:
            return NSLocalizedString("The requested URL is invalid.", comment: "")
        case .invalidSearchParameters:
            return NSLocalizedString("The URL parameters is invalid.", comment: "")
        case .invalidResults(let message):
            return NSLocalizedString(message, comment: "")
        case .invalidStatusCode(let message):
            return NSLocalizedString("Invalid HTTP status code:\(message ?? -1)", comment: "")
        }
    }
}

OMDb works with simple queries, it doesn’t have many end points! but to keep everything structured, let’s create another enumerator:

import Foundation

enum EndPoints {
    case Search
}

extension EndPoints {
    var path:String {
        let baseURL = "http://www.omdbapi.com"
        
        struct Section {
            static let search = "/?"
        }
        
        switch(self) {
        case .Search:
            return "\(baseURL)\(Section.search)"

        }
        
    }
    
}

The next step is testing:

import XCTest
@testable import OpenMovie

class OpenMovieTests: XCTestCase {
    
    private let networkService = NetworkService.shared()
    
    func testSearch() {
        let promise = expectation(description: "Search for batman movies")
        networkService.search(for: "batman", page: 1) { (searchObject, error) in
            XCTAssertNil(error)
            XCTAssertTrue(searchObject?.response == "True")
            promise.fulfill()
        }
        waitForExpectations(timeout: 2, handler: nil)
    }
    
    func testSearchByIMDBID() {
        let promise = expectation(description: "Search for Batman: Dark Night Returns")
        networkService.getMovie(with: "tt2313197") { (movieObject, error) in
            XCTAssertNil(error)
            XCTAssertTrue(movieObject?.imdbID == "tt2313197")
            promise.fulfill()
        }
     
        waitForExpectations(timeout: 2, handler: nil)
    }
    
}

And the results:

Now the last step, isn’t it better you look at it yourself? It’s TL;DR sort of thing, download it from github and give it a try.

This is how it looks: