Swift Completion & Loop Issue - swift

Trying to download a PKG file from one of three urls. The logic basically finds the latency of a download from each download url and sets the final download url to the host with the lowest latency.
import Cocoa
import Alamofire
// Create my object via Struct
struct Package {
var latency: Double?
var name: String?
var statuscode: Int?
var download: Bool?
var downloadUrl: String?
}
// Download the package from the provided download url and return the object
func getPKG(pkgName: String, dpUrl: String, completion: #escaping (Package) -> (Package)) {
let url = URL(string: "\(dpUrl)\(pkgName)")
let parameters: Parameters = ["foo":"bar"]
Alamofire.download(url, method: .get, parameters: parameters, encoding: JSONEncoding.default, to: destination)
.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
debugPrint("Download Progress...: \(progress.fractionCompleted)")
}
.validate(statusCode: 200..<399)
.response { response in
debugPrint(response.response!)
debugPrint(response.response!.statusCode)
debugPrint(response.timeline.latency)
let dlObject = Package(latency: response.timeline.latency, name: pkgName, statuscode: response.response?.statusCode, download: true, downloadUrl: dpUrl)
completion(dlObject)
}
}
var share_response = [String: Double]()
var package_sources: NSArray! = ["https://www.jss1.com/Share", "https://www.jss2.com/Share", "https://www.jss3.com/Share"]
let package_names: String = ["Dummy1.pkg", "Dummy2.pkg", "Dummy3.pkg"]
// Loop through the package sources and find the one with
// the lowest latency.
for share_url in package_sources {
getPKG(pkgName: "Dummy.pkg", dpUrl: share_url, completion: {
dlObject in
if dlObject.latency != nil {
share_response[share_url] = dlObject.latency
} else {
debugPrint("nothing yet")
}
return dlObject
})
}
let final_download_url = share_response.min { a, b in a.value < b.value }
// Here is where it breaks and responds with nil
for package in package_names {
let download_url = URL(string: final_download_url + package)
Download commands here...
}
This is done by looping through each download url and populating a dictionary with the key as the url and the value as the latency. When the script moves on to download from the "fastest" download url, it fails with nil.
I'm assuming that's because the script is moving on while the completion handler is still running and nothing is in the dictionary yet, but how would I address this?

Based on the answer from #vadian at Synchronous request using Alamofire
...
let group = DispatchGroup()
var share_response = [String: Double]()
var package_sources: NSArray! = ["https://www.jss1.com/Share", "https://www.jss2.com/Share", "https://www.jss3.com/Share"]
let package_names: String = ["Dummy1.pkg", "Dummy2.pkg", "Dummy3.pkg"]
// Loop through the package sources and find the one with
// the lowest latency.
for share_url in package_sources {
group.enter()
getPKG(pkgName: "Dummy.pkg", dpUrl: share_url, completion: {
group.leave()
dlObject in
if dlObject.latency != nil {
share_response[share_url] = dlObject.latency
} else {
debugPrint("nothing yet")
}
return dlObject
})
}
group.notify(queue: DispatchQueue.main) {
let final_download_url = share_response.min { a, b in a.value < b.value }
// Here is where it breaks and responds with nil
for package in package_names {
let download_url = URL(string: final_download_url + package)
Download commands here...
}
}

Related

Dependency Injection in Protocol/Extension

I am following along with this tutorial in order to create an async generic network layer. I got the network manager working correctly.
https://betterprogramming.pub/async-await-generic-network-layer-with-swift-5-5-2bdd51224ea9
As I try to implement more APIs, that I can use with the networking layer, some of the APIs require different tokens, different content in the body, or header etc, that I have to get at runtime.
In the snippet of code below from the tutorial, I get that we are building up the Movie endpoint based on .self, and then return the specific values we need. But the issue is, some of the data in this, for example, the access token, has to be hard coded here. I am looking for a way, that I can 'inject' the accessToken, and then it will be created with this new token. Again, the reason for this, is that in other APIs, the access token might not always be known.
protocol Endpoint {
var scheme: String { get }
var host: String { get }
var version: String? { get }
var path: String { get }
var method: RequestMethod { get }
var queryItems: [String: String]? { get }
var header: [String: String]? { get }
var body: [String: String]? { get }
}
extension MoviesEndpoint: Endpoint {
var path: String {
switch self {
case .topRated:
return "/3/movie/top_rated"
case .movieDetail(let id):
return "/3/movie/\(id)"
}
}
var method: RequestMethod {
switch self {
case .topRated, .movieDetail:
return .get
}
}
var header: [String: String]? {
// Access Token to use in Bearer header
let accessToken = "insert your access token here -> https://www.themoviedb.org/settings/api"
switch self {
case .topRated, .movieDetail:
return [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8"
]
}
}
var body: [String: String]? {
switch self {
case .topRated, .movieDetail:
return nil
}
}
For an example, I tried converting the var body to a function, so I could do
func body(_ bodyDict: [String, String]?) -> [String:String]? {
switch self{
case .test:
return bodyDict
}
The idea of above, was that I changed it to a function, so I could pass in a dict, and then return that dict in the api call, but that did not work. The MoviesEnpoint adheres to the extension Endpoint, which then gives the compiler error 'Protocol Methods must not have bodies'.
Is there a way to dependency inject runtime parameters into this Extension/Protocol method?
Change the declaration of MoviesEndpoint so that it stores the access token:
struct MoviesEndpoint {
var accessToken: String
var detail: Detail
enum Detail {
case topRated
case movieDetail(id: Int)
}
}
You'll need to change all the switch self statements to switch detail.
However, I think the solution in the article (four protocols) is overwrought.
Instead of a pile of protocols, make one struct with a single function property:
struct MovieDatabaseClient {
var getRaw: (MovieEndpoint) async throws -> (Data, URLResponse)
}
Extend it with a generic method to handle the response parsing and decoding:
extension MovieDatabaseClient {
func get<T: Decodable>(
endpoint: MovieEndpoint,
as responseType: T.Type = T.self
) async throws -> T {
let (data, response) = try await getRaw(endpoint)
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
switch response.statusCode {
case 200...299:
break
case 401:
throw URLError(.userAuthenticationRequired)
default:
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(responseType, from: data)
}
}
Provide a “live“ implementation that actually sends network requests:
extension MovieDatabaseClient {
static func live(host: String, accessToken: String) -> Self {
return .init { endpoint in
let request = try liveURLRequest(
host: host,
accessToken: accessToken,
endpoint: endpoint
)
return try await URLSession.shared.data(for: request)
}
}
// Factored out in case you want to write unit tests for it:
static func liveURLRequest(
host: String,
accessToken: String,
endpoint: MovieEndpoint
) throws -> URLRequest {
var components = URLComponents()
components.scheme = "https"
components.host = host
components.path = endpoint.urlPath
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8",
]
return request
}
}
extension MovieEndpoint {
var urlPath: String {
switch self {
case .topRated: return "/3/movie/top_rated"
case .movieDetail(id: let id): return "/3/movie/\(id)"
}
}
}
To use it in your app:
// At app startup...
let myAccessToken = "loaded from UserDefaults or something"
let client = MovieDatabaseClient.live(
host: "api.themoviedb.org",
accessToken: myAccessToken
)
// Using it:
let topRated: TopRated = try await client.get(endpoint: .topRated)
let movieDetail: MovieDetail = try await client.get(endpoint: .movieDetail(id: 123))
For testing, you can create a mock client by providing a single closure that fakes the network request/response. Simple examples:
extension MovieDatabaseClient {
static func mockSuccess<T: Encodable>(_ body: T) -> Self {
return .init { _ in
let data = try JSONEncoder().encode(body)
let response = HTTPURLResponse(
url: URL(string: "test")!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: nil
)!
return (data, response)
}
}
static func mockFailure(_ error: Error) -> Self {
return .init { _ in
throw error
}
}
}
So a test can create a mock client that always responds with a TopRated response like this:
let mockTopRatedClient = MovieDatabaseClient.mockSuccess(TopRated(...))
If you want to learn more about this style of dependency management and mocking, Point-Free has a good (but subscription required) series of episodes: Designing Dependencies.

Swift Test case Falling with Expectation

I am trying to run test case for Failure response . I have an empty json file into project and named it FailureResponse . This file is empty . I trying to count the number of array is empty for example ..
XCTAssertTrue(schools.count==0)
It should pass the test because the json file is empty .
same result fields like school name and School location etc but the problem is it showing error ..
testFailure(): Asynchronous wait failed: Exceeded timeout of 6 seconds, with unfulfilled expectations: "waiting for response".
View Model code...
import Foundation
import Combine
class ViewModel {
private let networkManager = NetworkManager()
#Published private(set) var school = [School]()
func getSchools() {
loadMoreSchools()
}
func loadMoreSchools() {
let newURL = NetworkURLs.baseURL
networkManager
.getModel([School].self, from: newURL) { [weak self] result in
switch result {
case .success(let schoolResponse):
self?.school = schoolResponse
print(schoolResponse)
case .failure(let error):
print(error)
}
}
}
func getSchoolName(by row: Int) -> String {
let schoolName = school[row]
return schoolName.schoolName.uppercased()
}
func getSchoolLocation(by row: Int) -> String {
return "\(school[row].location)"
}
}
Here is my Mock service call ..
class MockService: NetworkManagerProtocol {
var data: Data?
func getModel<Model>(_ type: Model.Type, from url: String, completion: #escaping (Result<Model, Alomafire_Project.NetworkError>) -> ()) where Model : Decodable, Model : Encodable {
if let data = data {
do {
let result = try JSONDecoder().decode(type, from: data)
completion(.success(result))
} catch (let error){
print(error)
}
}
}
}
Here is code for call the local Jason ..
func getData(json: String) throws -> Data {
guard let url = Bundle(for: Alomafire_ProjectTests.self).url(forResource: json, withExtension: "json")
else { return Data() }
return try Data(contentsOf: url)
}
Here is the test case ....
func testFailure() throws {
// Given
mockService.data = try getData(json: "FailureResponse")
var schools: [School] = []
let expectation = expectation(description: "waiting for response")
// When
viewModel?
.$school
.dropFirst()
.sink(receiveValue: { result in
schools = result
expectation.fulfill()
})
.store(in: &subscribers)
// viewModel?.getSchools()
// Then
waitForExpectations(timeout: 10.0)
XCTAssertTrue(schools.count==0)
}
Here is the debug result . it return 0 ..
Here is the screenshot of the result ..
You mention in the question that "the json file is empty." If that is the case, then this test will fail. The MockService assumes that the Data pulled from the json file will be decodable to the type requested. If it isn't the getModel(_:from:completion:) will never call the completion and the test will not complete in the specified time limit. Solve this by calling the completion closure even when the JSONDecoder response with an error.
Also, even if that mock emits the error properly, your ViewModel doesn't do anything with it that would cause the schools type to update.

Parse responseJSON to ObjectMapper

I'm currently making a migration from Android to iOS, better said Java to Swift, I got a generic response in JSON, but I'm not able to use it as an object and show it in the storyboard. I'm really new to Swift so I've been stuck for a while.
I've tried ObjectMapper and also JSON decode with no result at all.
I declared this response as I used in Java(Android)
class ResponseObjectMapper<T,R>: Mappable where T: Mappable,R:Mappable{
var data:T?
var message:String!
var error:R?
required init?(_ map: Map) {
self.mapping(map: map)
}
func mapping(map: Map) {
data <- map["data"]
message <- map["message"]
error <- map["error"]
}
}
class UserMapper :Mappable{
var email:String?
var fullName:String?
var id:CLong?
var phoneNumber:String?
var token:CLong?
required init?(_ map: Map) {
}
func mapping(map: Map) {
email <- map["email"]
fullName <- map["fullName"]
id <- map["id"]
phoneNumber <- map["phoneNumber"]
token <- map["token"]
phoneNumber <- map["phoneNumber"]
}
}
In my Android project I use the Gson dependency and I was able to use my JSON as an object
class ErrorMapper:Mappable{
var message:String?
var code:Int?
required init?(_ map: Map) {
}
func mapping(map: Map) {
message <- map["message"]
code <- map["code"]
}
}
This is the Alamofire that gave me the JSON.
func login(params: [String:Any]){Alamofire.request
("http://192.168.0.192:8081/SpringBoot/user/login", method: .post,
parameters: params,encoding: JSONEncoding.default, headers:
headers).responseJSON {
response in
switch response.result {
case .success:
let response = Mapper<ResponseObjectMapper<UserMapper,ErrorMapper>>.map(JSONString: response.data)
break
case .failure(let error):
print(error)
}
}
}
If I print the response with print(response) I got
SUCCESS: {
data = {
email = "vpozo#montran.com";
fullName = "Victor Pozo";
id = 6;
phoneNumber = 099963212;
token = 6;
};
error = "<null>";
message = SUCCESS;
}
and if I use this code I can got a result with key and value but I don't know how to use it as an object
if let result = response.result.value {
let responseDict = result as! [String : Any]
print(responseDict["data"])
}
console:
Optional({
email = "vpozo#gmail.com";
fullName = "Victor Pozo";
id = 6;
phoneNumber = 099963212;
token = 6;
})
I would like to use it in an Object, like user.token in a View Controller, probably I'm really confused, trying to map with generic attributes.
Type 'ResponseObjectMapper<UserMapper, ErrorMapper>' does not conform to protocol 'BaseMappable'
First of all you will need a Network Manager which uses Alamofire to make all your requests. I have made generalized one that looks something like this. You can modify it as you want.
import Foundation
import Alamofire
import SwiftyJSON
class NetworkHandler: NSObject {
let publicURLHeaders : HTTPHeaders = [
"Content-type" : "application/json"
]
let privateURLHeaders : HTTPHeaders = [
"Content-type" : "application/json",
"Authorization" : ""
]
enum RequestType {
case publicURL
case privateURL
}
func createNetworkRequestWithJSON(urlString : String , prametres : [String : Any], type : RequestType, completion:#escaping(JSON) -> Void) {
let internetIsReachable = NetworkReachabilityManager()?.isReachable ?? false
if !internetIsReachable {
AlertViewManager.sharedInstance.showAlertFromWindow(title: "", message: "No internet connectivity.")
} else {
switch type {
case .publicURL :
commonRequest(urlString: baseURL+urlString, parameters: prametres, completion: completion, headers: publicURLHeaders)
break
case .privateURL:
commonRequest(urlString: baseURL+urlString, parameters: prametres, completion: completion, headers: privateURLHeaders)
break
}
}
}
func commonRequest(urlString : String, parameters : [String : Any], completion : #escaping (JSON) -> Void , headers : HTTPHeaders){
print("urlString:"+urlString)
print("headers:")
print(headers)
print("parameters:")
print(parameters)
let url = NSURL(string: urlString)
var request = URLRequest(url: url! as URL)
request.httpMethod = "POST"
request.httpHeaders = headers
request.timeoutInterval = 10
let data = try! JSONSerialization.data(withJSONObject: parameters, options: JSONSerialization.WritingOptions.prettyPrinted)
let json = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
if let json = json {
print("parameters:")
print(json)
}
request.httpBody = json!.data(using: String.Encoding.utf8.rawValue)
let alamoRequest = AF.request(request as URLRequestConvertible)
alamoRequest.validate(statusCode: 200..<300)
alamoRequest.responseJSON{ response in
print(response.response?.statusCode as Any )
if let status = response.response?.statusCode {
switch(status){
case 201:
print("example success")
SwiftLoader.hide()
case 200 :
if let json = response.value {
let jsonObject = JSON(json)
completion(jsonObject)
}
default:
SwiftLoader.hide()
print("error with response status: \(status)")
}
}else{
let jsonObject = JSON()
completion(jsonObject)
SwiftLoader.hide()
}
}
}
}
After this when ever you need to make a request you can use this function. This will take in parameters if any needed and once the request is complete it will execute a call back function in which you can handle the response. The response here will be of SWIFTYJSON format.
func makeNetworkRequest(){
let networkHandler = NetworkHandler()
var parameters : [String:String] = [:]
parameters["email"] = usernameTextField.text
parameters["pass"] = passwordTextField.text
networkHandler.createNetworkRequestWithJSON(urlString: "http://192.168.0.192:8081/SpringBoot/user/login", prametres: parameters, type: .publicURL, completion: self.handleResponseForRequest)
}
func handleResponseForRequest(response: JSON){
if let message = response["message"].string{
if message == "SUCCESS"{
if let email = response["data"]["email"].string{
//Do something with email.
}
if let fullName = response["data"]["fullName"].string{
//Do something with fullName.
}
if let id = response["data"]["id"].int{
//Do something with id.
}
if let phoneNumber = response["data"]["phoneNumber"].int64{
//Do something with phoneNumber.
}
if let token = response["data"]["token"].int{
//Do something with token.
}
}else{
//Error
}
}
}
Hope this helps. Let me know if you get stuck anywhere.

When updating UI with json response, "Thread 1: Fatal error: Index out of range." is received - Swift

Attempting to update a menu item to return all fixtures from api.
I've got a list of fixtures being returned.
How do I go about updating the fixtureMenuItem in the MenuController with all fixtures returned from the JSON? I thought I might be able to do something along the lines of fixtureMenuItem.title = fixtures.description
, but I'm getting "Thread 1: Fatal error: Index out of range."
Model
struct LiveScores: Codable {
let success: Bool
let fixturesData: FixturesData?
enum CodingKeys: String, CodingKey {
case fixturesData = "data"
case success
}
}
struct FixturesData: Codable {
let fixtures: [Fixture]
let nextPage, prevPage: Bool
enum CodingKeys: String, CodingKey {
case fixtures
case nextPage = "next_page"
case prevPage = "prev_page"
}
}
struct Fixture: Codable, CustomStringConvertible {
let id, date, time, round: String
let homeName, awayName, location, leagueID: String
let homeID, awayID: Int?
enum CodingKeys: String, CodingKey {
case id, date, time, round
case homeName = "home_name"
case awayName = "away_name"
case location
case leagueID = "league_id"
case homeID = "home_id"
case awayID = "away_id"
}
var description: String {
return "\(time): \(homeName) vs. \(awayName)"
}
}
// MARK: Convenience initializers
extension LiveScores {
init(data: Data) throws {
self = try JSONDecoder().decode(LiveScores.self, from: data)
}
}
Menu Controller - this is where I want to update the fixture menu item, to include the time, home and away team names. "Here is where all the fixtures will be populated!" - this is the hardcoded text I wish to replace with the fixture data.
var fixtures = [Fixture]()
func updateScores() {
liveScoreApi.fetchFixtures()
if let fixtureMenuItem = self.Menu.item(withTitle: "Fixtures") {
fixtureMenuItem.title = "Here is where all the fixtures will be populated!"
// TODO - populate the UI with fixtures returned from JSON response
}
}
Fetch Fixtures - here's where the fixtures are retrieved.
func fetchFixtures() {
let session = URLSession.shared
let url = URL(string: "\(baseUrl)fixtures/matches.json?key=\
(apiKey)&secret=\(apiSecret)&date=2018-06-02")
let task = session.dataTask(with: url!) { data, response, err in
// check for a hard error
if let error = err {
NSLog("Live Scores Api Error: \(error)")
}
// check the response code
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200: // perfecto!
if let liveScores = try? LiveScores.init(data: data!),
let fixture = liveScores.fixturesData
{
NSLog("\(fixture)")
}
case 401: // unauthorised
NSLog("Live Score Api returned an 'unauthorised' response.")
default:
NSLog("Live Scores Api returned response: %d %#", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))
}
}
}
task.resume()
}
In this example fixture data there are 26 fixtures and I want to show all of these.
Variations of this question come up constantly on SO.
Async functions don't wait for their results to be available. You give them a callback, which is a closure (a block of code you provide) that gets executed once the operation is complete.
You should rewrite your fetchFixtures() function to take a completion handler, and then refactor your updateScores() function to pass the code that updates your menu item into the completion handler for FetchFixtures.
See my answer to the question in the thread below for a simple example of this approach:
Swift: Wait for Firebase to load before return a function
As Duncan said in his answer, the issue was that the results weren't actually available.
I've implemented a completion handler of handleCompletion: on the fetchFixtures() function, which takes a true/false value plus the fixtures data. This is then returned in each http response case as shown below:
func fetchFixtures(handleCompletion:#escaping (_ isOK:Bool,_ param:
FixturesData?)->()) {
let session = URLSession.shared
let url = URL(string: "\(baseUrl)fixtures/matches.json?key=\
(apiKey)&secret=\(apiSecret)&date=2018-06-04")
let task = session.dataTask(with: url!) { data, response, err in
// check for a hard error
if let error = err {
NSLog("Live Scores Api Error: \(error)")
}
// check the response code
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200: // perfecto!
if let liveScores = try? LiveScores.init(data: data!),
let fixture = liveScores.fixturesData
{
//NSLog("\(fixture)")
handleCompletion(true, fixture)
}
case 401: // unauthorised
NSLog("Live Score Api returned an 'unauthorised' response.")
handleCompletion(false, nil)
default:
NSLog("Live Scores Api returned response: %d %#", httpResponse.statusCode, HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))
handleCompletion(false, nil)
}
}
}
task.resume()
}
After implementing the above, I refactored the updateScores() to use this completion handler.
func updateScores() {
liveScoreApi.fetchFixtures() { (
isOK, fixture) in
if isOK == true {
if let fixtureMenuItem = self.Menu.item(withTitle: "Fixtures") {
fixtureMenuItem.title = (fixture?.fixtures.description)!
}
}
else {
NSLog("error fetching!")
}
}
}
The fixtureMenuItem now successfully displays the data if available.

How to instantiate a mapped class? (swift - alamofireObjectMapper)

I have this mapped class caled Movie and I make an API request that returns me this type. How can I instantiate this class with the values of my API response?
Movie mapped class:
class Movie: Mappable {
var posterURL : String?
var title : String?
var runtime : String?
var director : String?
var actors : String?
var genre : String?
var plot : String?
var production : String?
var released : String?
var year : String?
var imdbID : String?
var imdbRating : String?
required init?(map: Map) {
}
func mapping(map: Map) {
posterURL <- map["Poster"]
title <- map["Title"]
runtime <- map["Runtime"]
director <- map["Director"]
actors <- map["Actors"]
genre <- map["Genre"]
plot <- map["Plot"]
production <- map["Production"]
released <- map["Released"]
year <- map["Year"]
imdbID <- map["imdbID"]
imdbRating <- map["imdbRating"]
}
}
And in my MovieViewController I'm making the API call and passing the values for my outlet label.
But I would like to instantiate this class by assigning the values ​​obtained in my API call.
func getMovieById() {
let requestURL = "https://www.omdbapi.com/?i=\(String(describing: imdbID!))"
print("URL: \(requestURL)")
Alamofire.request(requestURL).responseObject{ (response: DataResponse<Movie>) in
print("|MovieController| Response is: \(response)")
DispatchQueue.main.async {
let spinnerActivity = MBProgressHUD.showAdded(to: self.view, animated: true)
spinnerActivity.label.text = "Loading";
spinnerActivity.isUserInteractionEnabled = false;
}
let movie = response.result.value
if let posterURL = movie?.posterURL {
print("Poster URL: \(posterURL)")
let imgStg: String = posterURL
print("---> Image string: \(imgStg)")
let imgURL: URL? = URL(string: imgStg)
let imgSrc = ImageResource(downloadURL: imgURL!, cacheKey: imgStg)
self.movPosterImageView.layer.cornerRadius = self.movPosterImageView.frame.size.width/2
self.movPosterImageView.clipsToBounds = true
//image cache with KingFisher
self.movPosterImageView.kf.setImage(with: imgSrc)
}
if let title = movie?.title {
print("Title: \(title)")
self.movTitleLabel.text = title
}
if let runtime = movie?.runtime {
print("Runtime: \(runtime)")
self.movRuntimeLabel.text = runtime
}
if let genre = movie?.genre {
print("Genre: \(genre)")
self.movGenreLabel.text = genre
}
if let plot = movie?.plot {
print("Plot: \(plot)")
self.movPlotTextView.text = plot
}
if let rating = movie?.imdbRating {
print("Rating: \(rating)")
self.movRatingLabel.text = rating
}
if let director = movie?.director {
print("Director: \(director)")
self.movDirectorLabel.text = director
}
if let production = movie?.production {
print("Production: \(production)")
self.movProductionLabel.text = production
}
if let actors = movie?.actors {
print("Actors: \(actors)")
self.movActorsLabel.text = actors
}
if let released = movie?.released {
print("Released in: \(released)")
self.movReleasedLabel.text = released
}
DispatchQueue.main.async {
MBProgressHUD.hide(for: self.view, animated: true)
}
}//Alamofire.request
}//getMovieByID()
It would be something like
let movieDetails: Movie = Movie(plot = movie?.plot, title = movie?.title, ...)
How can I do this with a mappable class?
Update
I'm trying to organize this things and also I'll have to reuse code, so did this inside functions seems better for me. So, I started separating the API call putting like this:
file: OMDB.swift
import Foundation
import Alamofire
import AlamofireObjectMapper
func getMovieIdFromAPI(imdbID: String, completionHandler: #escaping (Movie) -> () ) {
let requestURL = "https://www.omdbapi.com/?i=\(imdbID)"
print("|getMovieIdFromAPI| URL: \(requestURL)")
Alamofire.request(requestURL).responseObject{ (response: DataResponse<Movie>) in
print("|Alamofire request| Response is: \(response)")
if let movieResult = response.result.value{
completionHandler(movieResult)
}
}
}
Next step, I'm trying to create a MovieDAO, and here I'll have to instantiate my object, right? So, in the same file as my Movie class is, I've created a MovieDAO class with this function:
class MovieDAO {
func getMovieDetailed<Movie: Mappable>(imdbID: String, completionHandler: #escaping (Movie) -> ()) {
getMovieIdFromAPI(imdbID: imdbID, completionHandler: {
(movieResult) in
let mapper = Mapper<Movie>()
let movieDetailed = mapper.map(movieResult)!
completionHandler(movieDetailed)
})
}
}
But I didn't understood very well the answer and the xcode gives me an error in
let movieDetailed = mapper.map(movieResult)!
^Error: Argument labels '(_:)' do not match any available overloads
Could you explain how can I use the answer given in this case?
ObjectMapper is what helps you get an instance of the model class, with the property values set as per your API response. You will need to do the last step where in you tell ObjectMapper to do the 'mapping' procedure with the json you provide it.You can use this generic method to parse response for any Mappable class
static func parseModel<Model: Mappable>(modelResponse modelResponse: AnyObject, modelClass: Model.Type) -> Model? {
let mapper = Mapper<Model>()
let modelObject = mapper.map(modelResponse)!
return modelObject
}