Now that we have a wrapper struct for all the data needed to construct the url for our API request as well as the URL request object itself, we can proceed develop an API client class that will interact with the API REST endpoint. This class will be implemented as a singleton, and it will have a variable for a delegate that conforms to the OxfordDictionaryAPIDelegate protocol defined previously. The singleton will also act as a wrapper for URLSession's sharedSession singleton. That said, let's begin by defining our class as follows below:
class OxfordAPIClient: OxfordDictionaryAPIDelegate{ static let sharedClient = OxfordAPIClient() /** Instance variables **/ private var urlSession: URLSession! private var delegate: OxfordDictionaryAPIDelegate? private init(){ urlSession = URLSession.shared delegate = self } func setOxfordDictionaryAPIClientDelegate(with apiDelegate: OxfordDictionaryAPIDelegate){ self.delegate = apiDelegate } func resetDefaultDelegate(){ self.delegate = self } }
So far, we have our sharedClient singleton as well as getter/setter functions for the delegate. In order for the singleton to conform with the OxfordDictionaryAPIDelegateProtocol, we define an extension for the OxfordAPIClient which will contain the delegate methods that we have to implement:
//MARK: ********* Conformance to DictionaryAPIClient protocol methods extension OxfordAPIClient{ /** Unable to establish a connection with the server **/ internal func didFailToConnectToEndpoint(withError error: Error) { print("Error occurred while attempting to connect to the server: \(error.localizedDescription)") } /** Proper credentials are provided, the API request can be authenticated; an HTTP Response is received but no data is provided **/ internal func didFailToGetJSONData(withHTTPResponse httpResponse: HTTPURLResponse){ print("Unable to get JSON data with http status code: \(httpResponse.statusCode)") showOxfordStatusCodeMessage(forHTTPResponse: httpResponse) } /** Proper credentials are provided, and the API request is fully authenticated; an HTTP Response is received and the data is provided by the raw data could not be parsed into a JSON object **/ internal func didFailToSerializeJSONData(withHTTPResponse httpResponse: HTTPURLResponse){ print("Unable to serialize the data into a json response, http status code: \(httpResponse.statusCode)") showOxfordStatusCodeMessage(forHTTPResponse: httpResponse) } /** If erroneous credentials are provided, the API request can't be authenticated; an HTTP Response is received but no data is provided **/ iinternal func didFinishReceivingHTTPResponse(withHTTPResponse httpResponse: HTTPURLResponse){ print("HTTP response received with status code: \(httpResponse.statusCode)") showOxfordStatusCodeMessage(forHTTPResponse: httpResponse) } /** Proper credentials are provided, and the API request is fully authenticated; an HTTP Response is received and serialized JSON data is provided **/ internal func didFinishReceivingJSONData(withJSONResponse jsonResponse: JSONResponse, withHTTPResponse httpResponse: HTTPURLResponse) { print("JSON response received, http status code: \(httpResponse.statusCode)") showOxfordStatusCodeMessage(forHTTPResponse: httpResponse) print("JSON data received as follows:") print(jsonResponse) } func showOxfordStatusCodeMessage(forHTTPResponse httpResponse: HTTPURLResponse){ if let oxfordStatusCode = OxfordHTTPStatusCode(rawValue: httpResponse.statusCode){ let statusCodeMessage = oxfordStatusCode.statusCodeMessage() print("Status Code Message: \(statusCodeMessage)") } } }
As you can see, the singleton's implementation of these delegate methods is very minimal, logging the JSON responses, error messages, and HTTP status codes to the console. However, to provide more information for debugging purposes, we define a function showOxfordStatusCodeMessage(forHTTPResponse httpResponse: HTTPURLResponse) that will provide more detail information for the particular HTTP status code received from the server.
The primary responsibility for this API client will be to interact with REST API endpoint, so we define a method called startDataTask(withURLRequest: URLRequest) that will act as a wrapper for the URL shared session's singleton method startTask(with:completionHandler), whose completion handler processes input parameters of type NSData, URLResponse, and NSError. The wrapper function allows us to recast the URLResponse to an HTTPURLResponse, as well as to serialize the data into a JSON object. THe JSON object, HTTP status code obtained from the HTTPURLResponse, and the error objects are then passed to different delegate methods based on how the server responds to our API request. For example, via our delegate method, we can deal with a situation where a HTTP status code of 200 is obtained but where no data is returned from the server, or even the case where the data is provided by the server but fails to be parsed successfully for whatever reason. The implementation for this method is shown below:
/** Wrapper function for executing aynchronous download of JSON data from Oxford Dictionary API **/ private func startDataTask(withURLRequest request: URLRequest){ guard let delegate = self.delegate else { fatalError("Error: no delegate specified for Oxford API download task") } _ = self.urlSession.dataTask(with: request, completionHandler: { data, response, error in switch (data,response,error){ case (.some,.some,.none),(.some,.some,.some): //Able to connect to the server, data received let httpResponse = (response! as! HTTPURLResponse) if let jsonResponse = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! JSONResponse{ //Data received, and JSON serialization successful delegate.didFinishReceivingJSONData(withJSONResponse: jsonResponse, withHTTPResponse: httpResponse) } else{ //Data received, but JSON serialization not successful delegate.didFailToGetJSONData(withHTTPResponse: httpResponse) } break case (.none,.some,.none),(.none,.some,.some): //Able to connect to the server but failed to get data or received a bad HTTP Response if let httpResponse = (response as? HTTPURLResponse){ delegate.didFailToGetJSONData(withHTTPResponse: httpResponse) } break case (.none,.none,.some): //Unable to connect to the server, with an error received delegate.didFailToConnectToEndpoint(withError: error!) break case (.none,.none,.none): //Unable to connect to the server, no error received let error = NSError(domain: "Failed to get a response: Error occurred while attempting to connect to the server", code: 0, userInfo: nil) delegate.didFailToConnectToEndpoint(withError: error) break default: break } }).resume() }
Finally, we are not able to define publicly-accessible and human-readable functions that can interact with REST API endpoint based on the specific demands of the user. That is, the public accessible methods roughly correspond to the different kinds of API requests that can be made to the server. There is one caveat, however. From the previous tutorials, we understand that our OxfordAPIRequest struct has a method for generating URL request objects and that this is a throwing method, which means that our API client class must have a way of handling this possible error. Lo and behold, our delegate methods come to the rescue. If an error is thrown due to an invalid filter parameter, then we pass the error to our didFailToConnnectToEndpoint delegate method, which will log the error message to the console and notify the user that a connection could not be established because of an invalid filter parameter. Apart from the filters, validation for the other parameters is not necessary since they are either enum types or booleans, whose range of possible values is restricted.
func downloadWordlistJSONDataWithValidation(forFilters filters: [OxfordAPIEndpoint.OxfordAPIFilter]?, forLanguage language: OxfordAPILanguage = OxfordAPILanguage.English){ let apiRequest = OxfordAPIRequest(withEndpoint: OxfordAPIEndpoint.wordlist, withQueryWord: String(), withFilters: filters, withQueryLanguage: language) do { let urlRequest = try apiRequest.generateValidatedURLRequest() self.startDataTask(withURLRequest: urlRequest) } catch let error as NSError { guard let apiDelegate = self.delegate else { fatalError("Error: no delegate specified for Oxford API download task") } apiDelegate.didFailToConnectToEndpoint(withError: error) } }
The remainder of our API client functions are provided below, and they include functions for downloading lemmatron JSON data,, wordlist JSON data, diciontary entry data, as well as antonyms, synonyms, and example sentences derived from the thesaurus. These methods are provided below:
func downloadLemmatronJSONData(forHeadWord headWord: String, andWithFilters filters: [OxfordAPIEndpoint.OxfordAPIFilter]?){ let apiRequest = OxfordAPIRequest() let urlRequest = apiRequest.generateURLRequest() self.startDataTask(withURLRequest: urlRequest) } func downloadWordListJSONData(forDomainFilters domainFilters: [OxfordAPIEndpoint.OxfordAPIFilter], forRegionFilters regionFilters: [OxfordAPIEndpoint.OxfordAPIFilter], forRegisterFilters registerFilters: [OxfordAPIEndpoint.OxfordAPIFilter], forTranslationFilters translationFilters: [OxfordAPIEndpoint.OxfordAPIFilter], forLexicalCategoryFilters lexicalCategoryFilters: [OxfordAPIEndpoint.OxfordAPIFilter]){ let apiRequest = OxfordAPIRequest(withDomainFilters: domainFilters, withRegionFilters: regionFilters, withRegisterFilters: registerFilters, withTranslationsFilters: translationFilters, withLexicalCategoryFilters: lexicalCategoryFilters) let urlRequest = apiRequest.generateURLRequest() self.startDataTask(withURLRequest: urlRequest) } func downloadDictionaryEntryJSONData(forWord word: String, withFilters filters: [OxfordAPIEndpoint.OxfordAPIFilter]?){ let apiRequest = OxfordAPIRequest(withWord: word, withFilters: filters) let urlRequest = apiRequest.generateURLRequest() self.startDataTask(withURLRequest: urlRequest) } func downloadExampleSentencesJSONData(forWord word: String){ let apiRequest = OxfordAPIRequest(withWord: word, hasRequestedExampleSentencesQuery: true, forLanguage: OxfordAPILanguage.English) let urlRequest = apiRequest.generateURLRequest() self.startDataTask(withURLRequest: urlRequest) } func downloadThesaurusJSONData(forWord word: String, withAntonyms hasRequestedAntonyms: Bool, withSynonyms hasRequestedSynonyms: Bool){ let apiRequest = OxfordAPIRequest(withWord: word, hasRequestedAntonymsQuery: hasRequestedAntonyms, hasRequestedSynonymsQuery: hasRequestedSynonyms) let urlRequest = apiRequest.generateURLRequest() self.startDataTask(withURLRequest: urlRequest) }
Our API client class is finished, but our work is not finished! What good programmer writes code without testing it. With that in mind, we will create a test target and test classes for the OxfordAPIRequest struct and the OxfordAPIClient class in order to make sure our code is doing what it's supposed to. The delegate methods we've defined will prove ver convenient for that goal as well.
To continue, please click here
If you feel confused or are having trouble following, you can go back to the previous page or back to the table of contents table of contents.