0

I have prepared a Generic Service layer in my tutorial, where the responses differ from one API call to another and as i know generic is capable of handling this.

Yet i find it hard to call different APIs that has differnet responses.

For example the GetPosts --> Response[Array of Post Model] For Example Delete POst --> Response (Post Model)

Knowing that both calls reach the below in the service layer

else if let jsonArray = json as? [Any] {
   let object = Mapper<T>().mapArray( JSONObject: jsonArray)
   completion(.success(object!))
}

You can check the response of each call on this Page

Here below is the Service LAYER CLASS this works fine when i call the getPosts API since in the saervice layer i defined the completiuon as Array --> [T], an in the getPosts Function i expect response as [PostsModel]

But when i try to call the DeletePost i face some complications since the result should be PostModel and not an array of PostModel.

When i modify the Servicelayer to accept T and not [T] i face some complications..

Hope anyone can help


import Foundation
import ObjectMapper
import Alamofire

class ServiceLayer {
    
    class func request<T: Mappable>(router: Router, completion: @escaping (Result<[T], Error>) -> ()) {
        do {
            AF.request(try router.asURLRequest()).validate().responseData { (data) in
                print(data)
                let result = data.result
                switch result {
                case .success(let data):
                    do {
                        //Response as Dictionary
                        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? Any {
                            
                            if let jsonDict = json as? [String : Any] {
                                let object = Mapper<T>().map(JSON: jsonDict)
                                completion(.success(object as! [T]))
                            }

                            //Response as Array
                            else if let jsonArray = json as? [Any] {
                                let object = Mapper<T>().mapArray( JSONObject: jsonArray)
                                completion(.success(object!))
                            }

                            //Response as String
                            else if let _ = String(data: data, encoding: .utf8){
                            }
                        }
                    } catch {
                        print(error.localizedDescription)
                        completion(.failure(error))
                    }
                    break
                case .failure(let error):
                    print(error.localizedDescription)
                    completion(.failure(error))
                    break
                }
            }
        } catch {
            print(error.localizedDescription)
        }
    }
}

Here is the call to the APIs from the ViewModel

    func getPosts() {
        
        self.isLoading.value = true
        
        ServiceLayer.request(router: .getPosts(userId: 1)) { (result : Result<[PostsModel], Error>) in
            
            self.isLoading.value = false
            
            switch result {
            case .success(let baseModel):
                self.array = baseModel
                self.fetchPostsSucceded.value = true
                
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
    
    func deletePosts(postid: Int) {
        
        self.isLoading.value = true
        
        ServiceLayer.request(router: .deletePosts(id: postid)) { (result : Result<PostsModel, Error>) in
            
            self.isLoading.value = false
            
            switch result {
            case .success(let baseModel):
                print("")
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }

This is the PostModel Response, Knwoing that when i modify the Service layer completion to T and not [T] i get this

Class method 'request(router:completion:)' requires that '[PostsModel]' conform to 'Mappable'


import Foundation
import ObjectMapper

class PostsModel: Mappable, Codable {
    
    var userId : Int?
    var id : Int?
    var title : String?
    var body : String?

    required init?(map: Map) {

    }

    func mapping(map: Map) {

        userId <- map["userId"]
        id <- map["id"]
        title <- map["title"]
        body <- map["body"]
    }

}


Hope someone can help me resolve this, The needed result is to keep only one service layer that accepts any call and in the API calls to specify the return value.

  • 1
    What about using two generic types, one for Mappable and one for the return type or write another functions that returns T instead of [T] but internally is just a wrapper around your current function? – Joakim Danielson Jul 19 '21 at 10:15
  • 1
    as i see, you can only write T, not [T] because PostModel and [PostModel] are both Mappable. – goat_herd Jul 19 '21 at 10:16
  • @JoakimDanielson this is what i am trying to avoid actually, since The idea of generics is that it accpts anything, Please correct me if am wrong. – Mahmoud Zinji Jul 19 '21 at 10:34
  • this is what i get when i set the Servie layer with T and not [T]n adn try to decode Could not cast value of type 'Swift._ContiguousArrayStorage' (0x7f8eee826748) to 'Toothpick.PostsModel' (0x1074ed0d8). it crashes at the completion else if let jsonArray = json as? [Any] { let object = Mapper().mapArray( JSONObject: jsonArray) completion(.success(object! as! T)) }. knowing that the response is an array of 10 objects and it visible in Object @goat_herd – Mahmoud Zinji Jul 19 '21 at 10:55
  • I created a git for this i would really appreciate if you guys can give it a look when free. In case you want to upate the code as i do to resolve this i think its only in the Service Layer and in the MainVM. https://github.com/MahmoudZinji/ToothpickTest – Mahmoud Zinji Jul 19 '21 at 11:02
  • 1
    Accepts anything, well not if you need to accept two different things. Anyway I personally would start by trying the option with a second function to see if that works, it looks like a straightforward way to solve this. – Joakim Danielson Jul 19 '21 at 11:15
  • I aklready started doing this in the meanwhile, Thank You :) – Mahmoud Zinji Jul 19 '21 at 11:17
  • 1
    @MahmoudZinji Make yourself a favor and forget about ObjectMapper. Take a look at Codable protocol and refactor your code – Leo Dabus Jul 19 '21 at 16:56

1 Answers1

0

if your service request<T: Mappable> get one object (not array) you decode it as array anyway

if let jsonDict = json as? [String : Any] {
                                let object = Mapper<T>().map(JSON: jsonDict)
                                completion(.success(object as! [T])) //<-- array anyway
                            }

but in function deletePosts you espect as Result<PostsModel, Error> not Result<[PostsModel], Error>. so if your service can answering only arrays you should change your ServiceLayer.request(router: .deletePosts(id: postid)) { (result : Result<PostsModel, Error>) in to ServiceLayer.request(router: .deletePosts(id: postid)) { (result : Result<[PostsModel], Error>) in

EvGeniy Ilyin
  • 1,817
  • 1
  • 21
  • 38