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() //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: "&").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 URLQueryItem
s.
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) } }
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.
4 Comments
far
at 5 years agoCan you please share project github link since im not able to download from the github link you have mentioned in the post
admin
at 5 years agoHello Farooq, here you go https://github.com/maysamsh/openmoviedb-ios
Sindhu
at 3 years agoCan you please upload any project using TDmi api for movie search app
admin
at 3 years agoSorry I don’t have any.