How to create objects from SwiftyJSON
Asked Answered
S

4

18

I have a code, that parses JSON's list of questions and I can get every property. How can I iterate through the whole file and for each question create an object ?

class ViewController: UIViewController {

var hoge: JSON?

override func viewDidLoad() {
    super.viewDidLoad()

    let number = arc4random_uniform(1000)
    let url = NSURL(string: "http://www.wirehead.ru/try-en.json?\(number)")
    var request = NSURLRequest(URL: url!)
    var data = NSURLConnection.sendSynchronousRequest(request, returningResponse: nil, error: nil)
    if data != nil {
        hoge = JSON(data: data!)
        let level = hoge!["pack1"][0]["level"].intValue
        let questionText = hoge!["pack1"][0]["questionText"].stringValue
        let answer1 = hoge!["pack1"][0]["answer1"].stringValue
        let answer2 = hoge!["pack1"][0]["answer2"].stringValue
        let answer3 = hoge!["pack1"][0]["answer3"].stringValue
        let answer4 = hoge!["pack1"][0]["answer4"].stringValue
        let correctAnswer = hoge!["pack1"][0]["correctAnswer"].stringValue
        let haveAnswered = hoge!["pack1"][0]["haveAnswered"].boolValue

    }
  }
}

my model of Question which objects I want to create below

class Question {

    var level : Int?
    var questionText : String?
    var answer1 : String?
    var answer2 : String?
    var answer3 : String?
    var answer4 : String?
    var correctAnswer : String?
    var haveAnswered : Bool = false

    init(level: Int, questionText:String, answer1:String, answer2:String, answer3:String, answer4:String, correctAnswer: String, haveAnswered:Bool) {
        self.level = level
        self.questionText = questionText
        self.answer1 = answer1
        self.answer2 = answer2
        self.answer3 = answer3
        self.answer4 = answer4
        self.correctAnswer = correctAnswer
        self.haveAnswered = false
    }

}
Sopping answered 27/7, 2015 at 19:24 Comment(1)
I see you are declaring your properties (except haveAnswered) as optional. On the other hand all the parameters of your init are not optionals. Which one is correct?Packthread
P
13

This is how I would approach the problem.

Step 1

Since your init inside Question does receive non optional objects, I had the feeling that the properties of Questions should be non optional too. I also converted the properties from var to let (tell me if I am wrong).

Step 2

This is the refactored Question class. As you can see I added a class method build that receive a JSON (a SwiftyJSON) and returns a Question (if the json contains correct data), nil otherwise.

Right now I cannot do this with a failable initializer.

extension String {
    func toBool() -> Bool? {
        switch self.lowercaseString {
        case "true", "1", "yes" : return true
        case "false", "0", "no" : return false
        default: return nil
        }
    }
}

class Question {

    let level: Int
    let questionText: String
    let answer1: String
    let answer2: String
    let answer3: String
    let answer4: String
    let correctAnswer: String
    let haveAnswered: Bool

    init(level: Int, questionText:String, answer1:String, answer2:String, answer3:String, answer4:String, correctAnswer: String, haveAnswered:Bool) {
        self.level = level
        self.questionText = questionText
        self.answer1 = answer1
        self.answer2 = answer2
        self.answer3 = answer3
        self.answer4 = answer4
        self.correctAnswer = correctAnswer
        self.haveAnswered = false
    }

    class func build(json:JSON) -> Question? {
        if let
            level = json["level"].string?.toInt(),
            questionText = json["questionText"].string,
            answer1 = json["answer1"].string,
            answer2 = json["answer2"].string,
            answer3 = json["answer3"].string,
            answer4 = json["answer4"].string,
            correctAnswer = json["correctAnswer"].string,
            haveAnswered = json["haveAnswered"].string?.toBool() {
                return Question(
                    level: level,
                    questionText: questionText,
                    answer1: answer1,
                    answer2: answer2,
                    answer3: answer3,
                    answer4: answer4,
                    correctAnswer: correctAnswer,
                    haveAnswered: haveAnswered)
        } else {
            debugPrintln("bad json \(json)")
            return nil
        }
    }
}

Step 3

Now let's look at viewDidLoad.

func viewDidLoad() {
    super.viewDidLoad()

    let number = arc4random_uniform(1000)

    if let
        url = NSURL(string: "http://www.wirehead.ru/try-en.json?\(number)"),
        data = NSURLConnection.sendSynchronousRequest(NSURLRequest(URL: url), returningResponse: nil, error: nil) {
        // line #a
        let rootJSON = JSON(data: data) 
        // line #b
        if let questions = (rootJSON["pack1"].array?.map { return Question.build($0) }) {
            // now you have an array of optional questions [Question?]...
        }

    }
}

At line #a I put inside rootJSON the whole data received from the connection (converted into JSON).

What happen at line #b?

Well I try to access the array located inside "pack1".

rootJSON["pack1"].array?

If the array exists I run the map method. This will extract each cell of the array and I will be able to refer to it with the $0 parameter name inside the closure.

Inside the closure I use this json block (that should represent a question) to build a Question instance.

The result will be an array of Question?. There could be ill values if some son data was not valid. If you want I can show you how to remove the nil values from this array

I could not try the code with real data, hope this helps.

Packthread answered 27/7, 2015 at 20:36 Comment(8)
Excellent answer. I'm in the process of modeling a similar class and this really helped me out.Bichromate
@Bichromate Thank you. It'a solution I refined several times over the last year.Packthread
Thank you very much! Unfortunately i get bad json error. I checked JSON, it passes validation. Can u please have a look whats wrong ? the data is here wirehead.ru/try-en.jsonSopping
In the original JSON the level value is a String. So we cannot retrieve it directly as an Int. I am going to update my answer ;-)Packthread
same result, bad json :(Sopping
Ok maybe because haveAnswered is a String too (instead of a Bool). I updated my answer. Let's try again :)Packthread
Please note that in the original JSON, answer2 is spelled wrong 4 times (asnwer2 instead of answer2).Packthread
Oh, Im sorry for mistype. Now it works great! Thank you very much!!Sopping
M
10

You can use SwiftyJSONModel which was specifically designed for this purpose. So in your case the model would be like this:

class Question: JSONObjectInitializable {
    enum PropertyKey: String {
        case level, questionText
        case answer1, answer2, answer3, answer4
        case correctAnswer, haveAnswered
    }

    var level : Int?
    var questionText : String?
    var answer1 : String?
    var answer2 : String?
    var answer3 : String?
    var answer4 : String?
    var correctAnswer : String?
    var haveAnswered : Bool = false

    required init(object: JSONObject<PropertyKey>) throws {
        level = object.value(for: .level)
        questionText = object.value(for: .questionText)
        answer1 = object.value(for: .answer1)
        answer2 = object.value(for: .answer2)
        answer3 = object.value(for: .answer3)
        answer4 = object.value(for: .answer4)
        correctAnswer = object.value(for: .correctAnswer)
        haveAnswered = object.value(for: .haveAnswered) ?? false
    }   
}

And then do like this:

let rootJSON = JSON(data: data)
let questions = rootJSON.arrayValue.flatMap { try? Question(json: $0) }

The framework gives you several nice features:

  1. All the keys are stored in separated enum PropertyKey
  2. No boilerplate as stringValue, intValue etc.
  3. If JSON will be invalid, framework will give a verbose error and you will immediately see what exactly went wrong
Mieshamiett answered 8/3, 2017 at 16:39 Comment(0)
A
9

Step 1. We will create one protocol with one constructor method in it and Model class

protocol JSONable {
    init?(parameter: JSON)
}

class Style: JSONable {
    let ID              :String!
    let name            :String!

    required init(parameter: JSON) {
        ID            = parameter["id"].stringValue
        name          = parameter["name"].stringValue
    }

    /*  JSON response format
    {
      "status": true,
      "message": "",
      "data": [
        {
          "id": 1,
          "name": "Style 1"
        },
        {
          "id": 2,
          "name": "Style 2"
        },
        {
          "id": 3,
          "name": "Style 3"
        }
      ]
    }
    */
}

Step 2. We will create extension of JSON which will convert JSON to model class type object

extension JSON {
    func to<T>(type: T?) -> Any? {
        if let baseObj = type as? JSONable.Type {
            if self.type == .array {
                var arrObject: [Any] = []
                for obj in self.arrayValue {
                    let object = baseObj.init(parameter: obj)
                    arrObject.append(object!)
                }
                return arrObject
            } else {
                let object = baseObj.init(parameter: self)
                return object!
            }
        }
        return nil
    }
}

Step 3. Use code with Alamofire or other code.

Alamofire.request(.GET, url).validate().responseJSON { response in
        switch response.result {
            case .success(let value):
                let json = JSON(value)

                var styles: [Style] = []
                if let styleArr = json["data"].to(type: Style.self) {
                    styles = styleArr as! [Style]
                }
                print("styles: \(styles)")
            case .failure(let error):
                print(error)
        }
 }

I hope this will be useful.

Please refer to this link for more information on this.
https://github.com/SwiftyJSON/SwiftyJSON/issues/714

Afterbrain answered 9/11, 2016 at 9:47 Comment(1)
This answer is excellent. I was getting memory leaks without doing this, and this is actually the only solution that helped me! Thank you!Hypocoristic
B
1
for (item, content) in hoge {
    let level = content["level"].intValue
}

that should work

Bonnibelle answered 27/7, 2015 at 20:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.