How to create a data model - ios

I've recently started programming with swift and am currently creating an application, which receives json data from a server. I was wondering about the best way to create the data model. First I tried creating a class and if leting all of the properties, but I found it to be too cumbersome (I have objects with 10-12 proeprties):
class Favourite {
var mArtikelText: String = ""
var mRid: String = ""
init(object: [String: AnyObject]) throws {
if let text = object["text"] as? String {
self.mArtikelText = text
}
if let id = object["id"] as? String {
self.mRid = id
}
}
}
Then I read about the new guard keyword and decided to try with it:
enum JSONResponseError: ErrorType {
case NilResponseValue
}
class Favourite {
var mArtikelText: String
var mRid: String
init(object: [String: AnyObject]) throws {
guard let text = object["text"] as? String else {
self.mArtikelText = ""
throw JSONResponseError.NilResponseValue
}
guard let rid = object["id"] as? String else {
self.mRid = ""
throw JSONResponseError
}
self.mArtikelText = text
self.mRid = rid
}
}
But I get All stored properties of a class instance must be initialized before throwing from an initializer. I guess I could silence the error if I initialize the properties, when I declare them, as I did in the first example, but (correct me if I'm wrong) I thought the guard is used to avoid exactly this approach.
Then I tried guarding all the properties at once:
class Favourite {
var mArtikelText: String
var mRid: String
init(object: [String: AnyObject]) throws {
guard case let (text as String, rid as String) = (object["text"], object["id"]) else {
self.mArtikelText = ""
self.mRid = ""
throw JSONResponseError.NilResponseValue
}
self.mArtikelText = text
self.mRid = rid
}
}
But I don't like this approach, because I want to leave only the faulty values and work with the rest (sometimes I receive nil values, that I won't be using anyway, so there is no point in throwing the whole answer away).
So, my question is, what is the best way to create a reliable model class from a json dictionary?

Related

Two similar classes show different initialization errors

I found an error when I test some codes from Github.
class Profile {
let text: String
let date: String
let id: String?
init?(data: NSDictionary?) {
if let text = data?.valueForKeyPath(Test.Text) as? String {
self.text = text
if let date = data?.valueForKeyPath(Test.Created) as? String {
self.date = date
id = data?.valueForKeyPath(Test.ID) as? String
}
}else{
return nil
}
}
struct Test {
static let Text = "text"
static let Created = "created"
static let ID = "id"
}
}
The line of init? shows the error "constants self.data used before being initialized."
And I create a similar class of it, like
class Context {
let words: String
init?(text:String?) {
if let words = text {
self.words = words
}else{
return nil
}
}
}
This time it shows " all stored properties of class instance must be initialized before returing nil from an initializer."
For the first one , there is a workaround that I can delete the else block and give each properties an empty value would fix the error. However it would have me change the properties mutable.
( I don't want it to be mutable)
And for the second example, I just insert self.word = ""before the line of return nil could also fix the error.
But I really wonder why these similar cases show the different errors and realize the logic of Swift, and how can I fix it correctly?
Thank you for helping me.
Try this version of the code.
Code 1:
class Profile {
var text: String = ""
var date: String = ""
var id: String? = ""
init?(data: NSDictionary?) {
if let text = data?.valueForKeyPath(Test.Text) as? String {
self.text = text
if let date = data?.valueForKeyPath(Test.Created) as? String {
self.date = date
id = data?.valueForKeyPath(Test.ID) as? String
}
}else{
return nil
}
}
struct Test {
static let Text = "text"
static let Created = "created"
static let ID = "id"
}
}
Code 2:
class Context {
var words: String = ""
init?(text:String?) {
if let words = text {
self.words = words
} else {
return nil
}
}
}
York initializers are incomplete and that's why you get the message. Swift is type save, which means that all non optional properties must be initialized before the class is returned. Even when the class returns a nil. secondly, you can't call self (which is the class itself) if you haven't initialized the class. However, that should not be a problem in your case, since you've defined a root class. For your first class, please, implement the code like this and it should work.
class Profile {
struct Test {
static let Text = "text"
static let Created = "created"
static let ID = "id"
}
let text: String
let date: String
let id: String?
init?(data: NSDictionary?) {
guard let tempText = data?.valueForKeyPath(Test.Text) as? String else {
text = ""
date = ""
id = nil
return nil
}
text = tempText
if let tempDate = data?.valueForKeyPath(Test.Created) as? String {
date = tempDate
id = data?.valueForKeyPath(Test.ID) as? String
} else {
date = ""
id = nil
}
}
}
For the second class you need to do a similar thing, which means in the else statement give words a value and it should be okay.

iOS Creating an Object Best Practice

I've created a wizard for user's to sign up using my app, however, I'm having some doubts as to how I should store their information along the way.
I have a User model, which is filled out when users are pulled from the database, however, there are some required fields on this model that wouldn't be filled out if I were to use it as the object that is passed along as the user goes through the the wizard.
Here is my User model:
final class User: NSObject, ResponseObjectSerializable, ResponseCollectionSerializable {
let id: Int
var facebookUID: String?
var email: String
var firstName: String
var lastName: String
var phone: String?
var position: String?
var thumbnail: UIImage?
var timeCreated: CVDate
init?(response: NSHTTPURLResponse, var representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
representation = dataRepresentation
}
self.id = representation.valueForKeyPath("id") as! Int
self.facebookUID = (representation.valueForKeyPath("facebook_UID") as? String)
self.email = (representation.valueForKeyPath("email") as? String) ?? ""
self.firstName = (representation.valueForKeyPath("first_name") as? String) ?? ""
self.lastName = (representation.valueForKeyPath("last_name") as? String) ?? ""
self.phone = (representation.valueForKeyPath("phone") as? String)
self.position = (representation.valueForKeyPath("position_name") as? String)
self.thumbnail = UIImage(named: "ThomasBaldwin")
if let timeCreated = representation.valueForKeyPath("time_created") as? String {
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
if let date = formatter.dateFromString(timeCreated) {
self.timeCreated = CVDate(date: date)
} else {
self.timeCreated = CVDate(date: NSDate())
}
} else {
self.timeCreated = CVDate(date: NSDate())
}
}
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [User] {
var users: [User] = []
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [NSDictionary]) {
if let dataRepresentation = dataRepresentation as? [[String: AnyObject]] {
for userRepresentation in dataRepresentation {
if let user = User(response: response, representation: userRepresentation) {
users.append(user)
}
}
}
}
return users
}
}
Notice the variables id and timeCreated. These are both generated when a new row is added to the Users table in the database, therefore, I wouldn't have values for those variables until the user is actually created.
Also, I would like to add some methods to the model, such as validateUser which will be a method that makes sure all the fields are filled out, and validateEmail which will be a method that makes sure the email is in proper syntax, and so on...
My question is, should I
A. just make those constants optional and add those methods to my current User model
B. make another model called CreateUserModel that only has variables for the information the user will be filling out and put the extra methods in there
UPDATE
I updated my User class to use a dictionary as the storage mechanism and it already looks a lot cleaner. However, the issue that comes to mind is, how will another programmer know which fields he can grab from the User model since I'm not individually storing them as variables anymore. Would they just have to check the DB and look at the structure of the table?
Here's my updated User class:
final class User: NSObject, ResponseObjectSerializable, ResponseCollectionSerializable {
var properties = NSDictionary()
init?(response: NSHTTPURLResponse, representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
properties = dataRepresentation
}
properties = representation as! NSDictionary
}
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [User] {
var users: [User] = []
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [NSDictionary]) {
if let dataRepresentation = dataRepresentation as? [[String: AnyObject]] {
for userRepresentation in dataRepresentation {
if let user = User(response: response, representation: userRepresentation) {
users.append(user)
}
}
}
}
return users
}
}
I would make them Optionals. That is the beauty of Optionals - you can use nil to mean exactly "no data here".
The other grand strategy that comes to mind is to use a dictionary as the storage mechanism inside your model, because that way either it has a certain key or it doesn't. You could make your User object key-value coding compliant, and thus effectively transparent, by passing keys on to the dictionary.

Error handling parsing JSON in Swift

I'm using Alamofire and am parsing the returned JSON into an object as shown below:
final class User: NSObject, ResponseObjectSerializable {
var id: Int
var facebookUID: String?
var email: String
var firstName: String
var lastName: String
var phone: String?
var position: String?
var timeCreated: CVDate
init?(response: NSHTTPURLResponse, var representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
representation = dataRepresentation
}
if let id = representation.valueForKeyPath("id") as? Int {
self.id = id
} else {
self.id = 0
}
if let facebookUID = representation.valueForKeyPath("facebook_UID") as? String {
self.facebookUID = facebookUID
}
if let email = representation.valueForKeyPath("email") as? String {
self.email = email
} else {
self.email = ""
}
if let firstName = representation.valueForKeyPath("first_name") as? String {
self.firstName = firstName
} else {
self.firstName = ""
}
if let lastName = representation.valueForKeyPath("last_name") as? String {
self.lastName = lastName
} else {
self.lastName = ""
}
if let phone = representation.valueForKeyPath("phone") as? String {
self.phone = phone
}
if let position = representation.valueForKeyPath("position_name") as? String {
self.position = position
}
if let timeCreated = representation.valueForKeyPath("time_created") as? String {
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
if let date = formatter.dateFromString(timeCreated) {
self.timeCreated = CVDate(date: date)
} else {
self.timeCreated = CVDate(date: NSDate())
}
} else {
self.timeCreated = CVDate(date: NSDate())
}
}
}
My question is, is this style the best way to decode JSON and set the non-optional instance variables? For example, in this statement:
if let id = representation.valueForKeyPath("id") as? Int {
self.id = id
}
I am required by the compiler to add an else clause and set the id to something otherwise xCode throws an error saying: self.id is not initialized at implicitly generated super.init call.
But at the same time, intializing self.id with a value of 0 is wrong and doesn't help me at all.
But at the same time, intializing self.id with a value of 0 is wrong and doesn't help me at all.
If having a default value for self.id feels wrong, then you should make this property an Optional. That way you wouldn't have to add an else clause:
final class User: NSObject, ResponseObjectSerializable {
var id: Int?
var facebookUID: String?
var email: String
var firstName: String
var lastName: String
var phone: String?
var position: String?
var timeCreated: CVDate
init?(response: NSHTTPURLResponse, var representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
representation = dataRepresentation
}
if let id = representation.valueForKeyPath("id") as? Int {
self.id = id
}
...
Update
You said in the comments:
I always need to have an id for the user object though.
If you have to have this id property then the question is moot, you just have to do
let id = representation.valueForKeyPath("id") as! Int
and guarantee earlier that this value will exist.
Because if your object needs an ID, then you can't initialize it anyway if this value doesn't exist and if you don't want a default value.
You could use ?? to provide default values like this:
self.id = (representation.valueForKeyPath("id") as? Int) ?? 0
While the ResponseObjectSerializable code is a great example from the Alamofire project, it's really a better idea to use a dedicated JSON parsing library that has actual error states. This is far better than using optionals to represent error states, or having to provide a default value for every field just in case the response isn't correctly formed.
Although it has a bit of learning curve, I prefer to use Argo for my JSON parsing. Once you get the hang of it it makes JSON parsing practically bulletproof. Better yet, it's easy to integrate with Alamofire, especially version 3 that was released today.
To address your concern about not having an ID being an error condition, you could use a failable initializer. I did that in a recent project. Looks something like this:
let id: Int!
init? (inputJson: NSDictionary) {
if let id = inputJson["id"] as? Int {
self.id = id
} else {
// if we are initing from JSON, there MUST be an id
id = nil
cry(inputJson) // this logs the error
return nil
}
}
Of course, this means your code will need to accept that the initialization of your entire object may fail ..

What is the best way to parse JSON files and create Model objects and then map them to the Core Data database?

I have the following instance method in my class, where "jsonObj" is a Dictionary:
func getSurvey(countryId: String, languageId: String, surveyId: String, userId: String, completionHandler: ((ICSurvey!, NSError!) -> Void)?)
{
ICWebServicesManager.downloadSurvey("", languageId: "", surveyId: "", userId: "") {
(jsonObj, error) -> Void in
if completionHandler != nil
{
if error != nil
{
completionHandler!(nil, error)
}
else
{
let surveyJSONObject = jsonObj
let survey = ICSurvey()
if let surveyIdObj = surveyJSONObject["id"] as? Dictionary<String, String>
{
self.dateFormatter!.dateFormat = "y-M-d'T'HH:mm:ssZZZZ"
survey.id = ICSurveyId(
surveyId : surveyIdObj["survey_id"]!,
countryId : surveyIdObj["country_id"]!,
languageId : surveyIdObj["language_id"]!
)
survey.uri = surveyJSONObject["uri"] as? String
survey.title = surveyJSONObject["title"] as? String
survey.startDate = self.dateFormatter!.dateFromString(surveyJSONObject["start_date"] as! String)
survey.endDate = self.dateFormatter!.dateFromString(surveyJSONObject["end_date"] as! String)
survey.type = surveyJSONObject["type"] as? String
survey.questionGroups = Array()
if let questionGroupsJSONObjects = surveyJSONObject["question_groups"] as? Array<Dictionary<String, AnyObject>>
{
for questionGroupObj in questionGroupsJSONObjects
{
let questionGroup = ICQuestionGroup()
questionGroup.questions = Array()
questionGroup.text = questionGroupObj["text"] as? String
if let questionsArrayObj = questionGroupObj["questions"] as? Array<Dictionary<String, AnyObject>>
{
for questionObj in questionsArrayObj
{
var question = ICQuestion()
if let questionIdObj = questionObj["id"] as? Dictionary<String, String>
{
question.id = ICQuestionId(
questionId : questionIdObj["question_id"]!,
languageId : questionIdObj["language_id"]!
)
question.type = questionObj["type"] as? String
var requiredString = questionObj["required"]
as? String
question.required = (requiredString == "True" ? true : false)
question.setValue(questionObj["text"] as? String, forKey: "text")
if let questionResponseObj = questionObj["response"] as? Dictionary<String, AnyObject>
{
question.response = ICResponse(
type : questionResponseObj["type"] as! String,
value : questionResponseObj["value"] as! Int,
clientTimestamp : self.dateFormatter!.dateFromString(questionResponseObj["client_timestamp"] as! String)!
)
}
if let questionResponseObj = questionObj["options"] as? Array<Dictionary<String, AnyObject>>
{
question.options = questionResponseObj
}
}
questionGroup.questions!.append(question)
}
}
survey.questionGroups!.append(questionGroup)
}
}
}
self.currentSurvey = survey
completionHandler!(self.currentSurvey, error)
}
}
}
}
After doing some code review with my mentor, he told me everything is pretty good, except I need to "abstract out hard wired properties". My understanding is that he does not want to have code that looks like this:
survey.startDate = self.dateFormatter!.dateFromString(surveyJSONObject["start_date"] as! String)
, because "start_date" for instance is hard-coded.
Instead, I should find what objects the JSON file represent and map that data correspondingly. While I agree to that to some extent, as the app code will not need many changes if it do it that way, it seem to be an overhead for me because I need to map everything to Core Data, and if a property changes, many things may change or crash.
My question is: What is the best way to parse JSON files and create Model objects and then map them to the Core Data database?
How do we have to "abstract out hard wired properties"?
If anyone has more experience with Web Services integration, please give me some advice.
Thanks in advance!
I was using SwiftyJSON on previous projects but now I've converted everything to Swift 1.2 (if let,) syntax and I honestly like it more since it feels like I have more flexibility and it alleviates 3rd party framework dependency.
As far as your "hard-coded" values, simply utilize optionals for testing. For instance, the following line requires a value to be present. Test for that value prior to unwrapping it. His fear is that if that object doesn't exist for whatever reason your program will crash. (This isn't tested but you should get the idea)
surveyId : surveyIdObj["survey_id"]!,
if let surveyId = surveyIdObj["survey_id"] as? String,
let countryId = surveyIdObj["country_id"] as? String,
let languageId = surveyIdObj["language_id"] as? String {
survey.id = ICSurveyId(
// You could also test here for each and add the values to your survey object
surveyId : surveyIdObj["survey_id"]!,
countryId : surveyIdObj["country_id"]!,
languageId : surveyIdObj["language_id"]!
)
}
Personally I prefer just creating a new NSManagedObject and assigning the values directly.
var survey = NSEntityDescription.insertNewObjectForEntityForName("Survey", inManagedObjectContext: self.appDelegate.managedObjectContext!) as! Survey
// Then just assign the values to the object using dot notation
survey.surveyId = surveyIdObj["survey_id"]!
// and so on
// Then save the context
self.managedObjectContext?.save(nil)

Type 'String' does not conform to protocol 'NSCopying' error when downcast AnyObject to String

I'm tring to parse a JSON format like this:
{
"key_1" : {
"key_2" : "value"
}
}
and then assign "value" to a variable.
Here is my code:
var variableShouldBeAssigned: String
if let x = (jsonResult["key_1"]? as? NSDictionary) {
if let y = (x["key_2"]? as? String) {
variableShouldBeAssigned = y
}
}
However, an error occurs when I try to downcast from x["key_2"]? to a String, but it's fine to downcast from jsonResult["key_1"]? to an NSDictionary.
I can solve the error by using x["key_2"] to replace x["key_2"]?, but I don't really know why it only works for jsonResult["key_1"]?.
Can anybody tell me the reason?
String does not conform to NSCopying, but surely NSString does!
Also, going from NSString to String is instantaneously implied...
So I would say try something like this... Change String to NSString
here is a sample, assuming that you handle the jsonResult as a NSDictionary...
func giveDictionary(jsonResult:NSDictionary) -> String?
{
if let x = (jsonResult["key_1"]? as? NSDictionary)
{
if let y = (x["key_2"]? as? NSString)
{
return y
}
}
return nil
}
You can simplify all your type checking by using a Swift dictionary at the beginning:
var variableShouldBeAssigned: String
if let dict = jsonResult as? [String:[String:String]] {
if let key1Dict = dict["key_1"] {
if let value = key1Dict["key_2"] {
variableShouldBeAssigned = value
}
}
}
In fact, you can even combine the two last if statements:
var variableShouldBeAssigned: String
if let dict = jsonResult as? [String:[String:String]] {
if let value = dict["key_1"]?["key_2"] {
variableShouldBeAssigned = value
}
}
In general, you should using Swift Dictionaries instead of NSDictionary

Resources