SimpiMind SimpiMind - 17 days ago 8
JSON Question

Refactoring Function for json parser Swift

I have a function i use for parsing json which works fine, however instead of me repeating things, i am looking for a way to refactor it such that i can pass any of my model object to the function's closure and it should still perform the same way.

This are the list of object i have to pass into the fun


  1. Video

  2. Event

  3. Trend

  4. News



The function below takes Video as parameter for the closure

func fetchFeedForUrlString(urlString: String, completion: @escaping ([Video]) -> ()){

let url = NSURL(string: urlString)!

URLSession.shared.dataTask(with: url as URL) { (data, responseUrl, error) -> Void in
if error != nil{
print("Unable to fetch data\(error)")
return
}

if let wrappedData = data,
let json = try? JSONSerialization.jsonObject(with: wrappedData, options: .allowFragments) as? [String: Any]{

if let results = json?["data"] as? [[String: AnyObject]]{
let videos = results.map({return Video(dictionary: $0)})

DispatchQueue.main.async {
completion(videos)
}
}

}

}.resume()
}

Answer

Actually it's a bit hard to tell you how to refactor it, but firstly I'd say you should declare a JSONDecodable protocol, or kind of. In this way you can define a protocol that describes object that can be initialized by a JSON.

protocol JSONDecodable {
    init?(_ json: [String: Any])
}

The conform the Video class to JSONDecodable protocol

class Video: JSONDecodable {
    init?(_ json: [String: Any]) {
        // your init code here. It can return nil in case the JSON is invalid
    }
}

Then you can use Swift generics to create a generic function

func fetchFeedForUrlString<T: JSONDecodable>(urlString: String, completion: @escaping     ([T]) -> ()){

    let url = NSURL(string: urlString)!

    URLSession.shared.dataTask(with: url as URL) { (data, responseUrl, error) -> Void in
        if error != nil{
            print("Unable to fetch data\(error)")
            return
        }

        if let wrappedData = data,
            let json = try? JSONSerialization.jsonObject(with: wrappedData, options: .allowFragments) as? [String: Any]{

            if let results = json?["data"] as? [[String: AnyObject]]{
                let videos = results.flatMap({return T($0)})

                DispatchQueue.main.async {
                    completion(videos)
                }
            }

        }

    }.resume()
}

Be careful because in this way you have to declare two different functions for parsing a set of JSONDecodable objects or only one.

To use it, simply write this

Instance.fetchFeedForUrlString(...) { (videos as [Video]) in
    /// your code here
}

Writing (videos as [Video]) you tell the compiler that the T parameter is a Video type, otherwise it cannot infer the type somewhere else. This implementation isn't the best and it's still improvable.