My server returns an array of photo informations in JSON like that :
"pics":[{"ID":182,"ID_member":39,"fn":"b69ea6f6c88b58c67a331aa3c5eaff81.jpg"}, ...]
I have a struct init function made to handle one photo json raw array (from type [String:Any]) :
init?(fromRaw _img:[String:Any]?)
{
guard
let img = _img,
let id = img["ID"] as? Int,
let idm = img["ID_member"] as? Int,
let fn = img["fn"] as? String
else
{
OOTLog.info("Warning : unable to init from photo raw array")
return nil ;
}
self.id = id
self.idMembre = idm
self.fileName = fn
}
My question is : lets say we have a json from server (of type [[String:Any]], array of n raw photos), is there any way to "overload" as? [Photo] with my init?(fromRaw:) within Photo struct, so we could just code :
guard let arrayPhoto = jsonRaw as? [Photo] else ..
Instead of :
guard let arrayPhotoRaw = jsonRaw as [[String:Any]] else ..
let photoArray:[Photo] = []
for p in jsonRaw {
guard let p = Photo(fromRaw:p) else { continue }
photoArray.append(p)
}
It's better to use
struct Root: Codable {
let pics: [Pic]
}
struct Pic: Codable {
let id, idMember: Int
let fn: String
enum CodingKeys: String, CodingKey {
case id = "ID"
case idMember = "ID_member"
case fn
}
}
let res = try! JSONDecoder().decode(Root.self, from:data)
print(res.pics)
Related
I have 2 structs codable : Student and Adress (which is linked to Student)
On my app, I fetch data from Firebase RTDB and then I store it with userdefault
Let's say a student changes his email and I just want to update the stored userdefault only with that updated email.
Do I need to specify all other data when I want to store it(name,adress,dob...) or can I just only update/store the email on my userdefault without specifying the other data ?
struct Adress : Codable, Identifiable {
var id: String { Student_UID }
var Student_UID: String
var Student_Street: String
var Student_Country: String
var Student_City: String
enum CodingKeys: String, CodingKey {
case Student_UID = "Adress_Student_UID"
case Student_Street = "Student_Street"
case Student_Country = "Student_Country"
case Student_City = "Student_City"
}
}
struct Student_Profile_Data:Codable, Identifiable {
var id: String { Student_UID }
var Student_UID: String
var Student_firstName: String?
var Student_username : String?
var Student_Email : String?
var Student_lastName: String
var Student_Address: Adress?
var Student_DOB: String?
var Student_Studentpoints : Int?
enum CodingKeys: String, CodingKey {
case Student_UID = "Student_UID"
case Student_firstName = "Student_firstName"
case Student_username = "Student_username"
case Student_Email = "Student_Email"
case Student_lastName = "Student_lastName"
case Student_Address = "Student_Address"
case Student_DOB = "Student_DOB"
case Student_Studentpoints = "Student_Studentpoints"
}
}
The USERDEfault part:
//READ
NSLog("userdefault TEST READ")
let defaults = UserDefaults.standard
if let savedStudent = defaults.object(forKey: "SavedStudent") as? Data {
let decoder = JSONDecoder()
if let loadedStudent = try? decoder.decode(Student_Profile_Data.self, from: savedStudent) {
NSLog("TEST PROFILE- username : \(loadedStudent.Student_username)")
}}
//WRITE
let add1 = Adress(Student_UID: "", Student_Street: "", Student_Country: "", Student_City: "")
let stud = Student_Profile_Data(Student_firstName: "", Student_username: "Martin", Student_Email: "", Student_lastName: "", Student_Address: add1, Student_DOB: "", Student_Studentpoints: 0, Student_UID: "")
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(stud) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "SavedStudent")
NSLog("WRITE OK »)
}
//UPDATE ONE (OR TWO) VALUES ?
If you are storing the student in UserDefault in this way, then updating it would involve the three-step process of reading the student, updating its value, then writing it back.
// read
let defaults = UserDefaults.standard
let decoder = JSONDecoder()
var savedStudent = defaults.data(forKey: "SavedStudent").flatMap {
try? decoder.decode(StudentProfileData.self, from: $0)
} ?? StudentProfileData(...) // some "empty" student to use when there is no previously saved student
// update
savedStudent.firstName = "John"
// write
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(savedStudent) {
defaults.set(encoded, forKey: "SavedStudent")
}
To make this more convenient, you can extract this as a function:
func updateSavedStudent(updateBlock: (inout StudentProfileData) -> Void) {
// read
let defaults = UserDefaults.standard
let decoder = JSONDecoder()
var savedStudent = defaults.data(forKey: "SavedStudent").flatMap {
try? decoder.decode(StudentProfileData.self, from: $0)
} ?? StudentProfileData(...) // some "empty" student to use when there is no previously saved student
// update
updateBlock(&savedStudent)
// write
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(savedStudent) {
defaults.set(encoded, forKey: "SavedStudent")
}
}
// usage:
updateSavedStudent {
$0.firstName = "John"
$0.lastName = "Smith"
}
Alternatively, make a computed property for this saved student and put it in a utility class somewhere. Do note that this will encode and decode the student once for every property you update though.
static var savedStudent: StudentProfileData {
get {
let defaults = UserDefaults.standard
let decoder = JSONDecoder()
return defaults.data(forKey: "SavedStudent").flatMap {
try? decoder.decode(StudentProfileData.self, from: $0)
} ?? StudentProfileData(...) // some "empty" student to use when there is no previously saved student
}
set {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(newValue) {
defaults.set(encoded, forKey: "SavedStudent")
}
}
}
// usage
savedStudent.firstName = "John"
I mostly work with dictionaries since I am fairly new but here we have an embedded struct that I need to loop through. With this, I need to be able to populate expanding cells in UITableView.
My struct looks like this:
struct Complain: Codable {
let eMail, message, timeStamp, userEmail: String
let status: Bool
let planDetails: PlanDetails
enum CodingKeys: String, CodingKey {
case eMail = "E-mail"
case message = "Message"
case timeStamp = "Time_Stamp"
case userEmail = "User_Email"
case status, planDetails
}
}
// MARK: - PlanDetails
struct PlanDetails: Codable {
let menuItemName: String
let menuItemQuantity: Int
let menuItemPrice: Double
}
And my query looks like this:
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
for document in documentSnapshot!.documents {
let eMail = document.get("E-mail")
let message = document.get("Message")
let timeStamp = document.get("Time_Stamp")
let userEmail = document.get("User_Email")
let planDetails = document.get("planDetails")
// how to loop through planDetails
}
}
I have gotten this far:
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
guard let planDetails = document.get("planDetails") as? [[String : Any]] else {
let email = document.get("E-mail")
let message = document.get("Message")
let timeStamp = document.get("Time_Stamp")
let userEmail = document.get("User_Email")
let status = false
}
for plan in planDetails {
if let menuItemName = plan["menuItemName"] as? String {
// not sure I populate the menuItemsName, menuItemsQuantity and menuItemsPrice within the array
}
}
}
}
}
UPDATE 2:
Final query: This is finally working as expected. But am not able to populate the expanding cells.
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let message = document.get("Message") as? String,
let timeStamp = document.get("Time_Stamp") as? String,
let userEmail = document.get("User_Email") as? String,
let status = document.get("status") as? Bool else {
continue
}
for plan in planDetails {
guard let menuItemName = plan["menuItemName"] as? String,
let menuItemQuantity = plan["menuItemQuantity"] as? Int,
let menuItemPrice = plan["menuItemPrice"] as? Double else {
continue
}
let planDetails = PlanDetails(menuItemName: menuItemName, menuItemQuantity: menuItemQuantity, menuItemPrice: menuItemPrice)
let complaint = Complain(eMail: email, message: message, timeStamp: timeStamp, userEmail: userEmail, status: status, planDetails: planDetails)
self.messageArray.append(complaint)
print(self.messageArray)
}
}
}
}
EDIT
// OPTION 1 - FULL REQUIREMENT
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
// all of these properties are required to parse a single document
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let message = document.get("Message") as? String,
let timeStamp = document.get("Time_Stamp") as? String,
let userEmail = document.get("User_Email") as? String,
let status = document.get("status") as? Bool else {
continue // continue this loop
}
for plan in planDetails {
// all of these properties are required to instantiate a struct
guard let name = plan["menuItemName"] as? String,
let price = plan["menuItemName"] as? Double,
let quantity = plan["menuItemQuantity"] as? Int else {
continue // continue this loop
}
let plan = PlanDetails(menuItemName: name, menuItemQuantity: quantity, menuItemPrice: price)
// then you will likely append this plan to an array
}
}
}
}
// OPTION 2 - PARTIAL REQUIREMENT
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
// all of these properties are required to parse a single document
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let message = document.get("Message") as? String else {
continue // continue this loop
}
// these properties are not required
let timeStamp = document.get("Time_Stamp") as? String // optional (can be nil)
let userEmail = document.get("User_Email") as? String // optional (can be nil)
let status = document.get("status") as? Bool ?? false // not optional because it has a default value of false
for plan in planDetails {
// all of these properties are required to instantiate a struct
guard let name = plan["menuItemName"] as? String,
let price = plan["menuItemName"] as? Double,
let quantity = plan["menuItemQuantity"] as? Int else {
continue // continue this loop
}
let plan = PlanDetails(menuItemName: name, menuItemQuantity: quantity, menuItemPrice: price)
// then you will likely append this plan to an array
}
}
}
}
There are a number of other ways you can do this, these are just a couple. For example, you can instantiate the struct with the [String: Any] dictionary itself.
I finally figured out what the problem was. Had to do with the way I setup the struct and how I populate the array.
Right way is as such i.e. the planDetails need to be cast as an array:
struct Complain: Codable {
let eMail, message, timeStamp, userEmail: String
let status: Bool
let planDetails: [PlanDetails]
enum CodingKeys: String, CodingKey {
case eMail = "E-mail"
case message = "Message"
case timeStamp = "Time_Stamp"
case userEmail = "User_Email"
case status, planDetails
}
}
// MARK: - PlanDetails
struct PlanDetails: Codable {
let menuItemName: String
let menuItemQuantity: Int
let menuItemPrice: Double
}
And then I need to iterate through each plan item, add it to the array and then add the array to the top level Complain:
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let status = document.get("status") as? Bool else {
continue
}
for plan in planDetails {
guard let menuItemName = plan["menuItemName"] as? String,
let menuItemQuantity = plan["menuItemQuantity"] as? Int else {
continue
}
let singlePlan = PlanDetails(menuItemName: menuItemName, menuItemQuantity: menuItemQuantity)
self.planDetailsArray.append(singlePlan)
}
let complain = Complain(eMail: email, status: status, planDetails: self.planDetailsArray)
self.planDetailsArray.removeAll()
self.messageArray.append(complain)
}
}
// Print the main array or load table
}
}
bsod's answer stays as the correct answer because without his help, I would not have been able to arrive at his conclusion.
I'm retrieving a JSON dict with the following structure:
"SITES": [
{
"NAME": “ALICE LANE”,
"WEBSITEAPPID": “XYZ”
}
]
}
I'm saving that straight into user defaults like this:
UserDefaults.standard.set(JSON(dict as Any)["SITES"].stringValue, forKey: "adminSites")
I know there is data in the JSON because the below code provides two rows of array data:
if let arraySites = dict?["SITES"] as? [[String: Any]] {
for sitesDetails in arraySites {
print(sitesDetails["NAME"] as Any)
print(sitesDetails["WEBSITEAPPID"] as Any)
}
}
When I try print the user default data count, I get 0
let defaultAdminSites = defaults.object(forKey: "adminSites") as? [String] ?? [String]()
print(defaultAdminSites.count)
Why am I getting 0 results? How would get the array row details if I did for ["NAME"] and ["WEBSITEAPPID"]?
I would recommend using a model object and Codable to avoid all those casts:
struct Model: Codable {
let sites: [Site]
enum CodingKeys: String, CodingKey {
case sites = "SITES"
}
}
struct Site: Codable {
let name, websiteAppid: String
enum CodingKeys: String, CodingKey {
case name = "NAME"
case websiteAppid = "WEBSITEAPPID"
}
}
// write to defaults
let model = Model(sites: [Site(name: "foo", websiteAppid: "bar")])
do {
let siteData = try JSONEncoder().encode(model)
UserDefaults.standard.set(siteData, forKey: "adminSites")
} catch {
print(error)
}
// read from defaults
if let siteData = UserDefaults.standard.data(forKey: "adminSites") {
do {
let model = try JSONDecoder().decode(Model.self, from: siteData)
for site in model.sites {
print(site.name, site.websiteAppid)
}
} catch {
print(error)
}
}
UserDefault Save Json Response
let result = jsonDict["SITES"] as? NSArray ?? []
let data = try! NSKeyedArchiver.archivedData(withRootObject: result, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "adminSites")
Get UserDefault data
let result = UserDefaults.standard.data(forKey: "adminSites")
if result != nil{
let data = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(result!) as? NSArray ?? []
print("Current User Details : - \(data)")
}
I create a very simple json data for practice, but it always decode error when JSONDecoder().decode. I try some way to modify my struct, but all gets same error(prints "error0"). The code is at below.
struct ss : Codable {
var a : String
var b : String
}
let js = "[{\"a\":\"1\",\"b\":\"2\"},{\"c\":\"3\",\"d\":\"4\"}]"
let data = js.data(using: .utf8)
let a = [ss].self
do {
if let s = try? JSONDecoder().decode(a, from : data!) {
print(s[0].a)
}else{
print("error0")
}
}catch{
print("error1")
}
There is a problem with your JSON replace
let js = "[{\"a\":\"1\",\"b\":\"2\"},{\"c\":\"3\",\"d\":\"4\"}]"
with
let js = "[{\"a\":\"1\",\"b\":\"2\"},{\"a\":\"3\",\"b\":\"4\"}]"
The other dictionary don't have keys a and b and that's why JSONDecoder is not able to decode Now your update code will be:
struct ss : Codable {
var a : String
var b : String
}
let js = "[{\"a\":\"1\",\"b\":\"2\"},{\"a\":\"3\",\"b\":\"4\"}]"
let data = js.data(using: .utf8)
let a = [ss].self
do {
let jsonDecoder = JSONDecoder()
let s = try jsonDecoder.decode(a, from: data!)
print(s[0].a) //"1\n"
} catch {
print(error)
}
PS: As #Milander suggested If you don't want to fix JSON you can make optional properties in your Struct like
struct ss : Codable {
let a, b, c, d: String?
}
You can define additional keys like below:
No optional No replacement
struct ss : Codable {
var a : String
var b : String
init(from decoder: Decoder) throws {
if let con = try? decoder.container(keyedBy: CodingKeys.self), let a = try? con.decode(String.self, forKey: .a), let b = try? con.decode(String.self, forKey: .b) {
self.a = a
self.b = b
} else if let con = try? decoder.container(keyedBy: AdditionalInfoKeys.self), let c = try? con.decode(String.self, forKey: .c), let d = try? con.decode(String.self, forKey: .d) {
a = c
b = d
} else {
throw NSError(domain: "Decoding error", code: 123, userInfo: nil)
}
}
enum AdditionalInfoKeys: String, CodingKey {
case c, d
}
}
I am not an iOS dev and have to make a few changes to a Swift / AlamoFire project (not mine) and am a bit lost.
I have the following JSON:
{"metro_locations":
[
{
"name":"Ruby Red"
},
{
"name":"Blue Ocean"
}
]
}
class (I know that there are issues here):
class Location{
var name=""
init(obj:tmp){
self.name=tmp["name"]
}
}
and need to make an AlamoFire call
Alamofire.request(.GET, "https://www.domain.com/arc/v1/api/metro_areas/1", parameters: nil)
.responseJSON { response in
if let dataFromNetworking = response.result.value {
let metroLocations = dataFromNetworking["metro_locations"]
var locations: [Location]=[]
for tmp in metroLocations as! [Dictionary] { // <- not working, Generic Paramter 'Key' could not be inferred
let location=Location.init(obj: tmp)
locations.append(location)
}
}
}
I have included the error msg, the "not working" but feel that there are issues in other parts too (like expecting a dictionary in the initialization). What does the 'Key' could not be inferred mean and are there other changes I need to make?
edit #1
I have updated my Location to this to reflect your suggestion:
init?(dictionary: [String: AnyObject]) {
guard let id = dictionary["id"] else { return nil }
guard let name = dictionary["name"] else { return nil }
guard let latitude = dictionary["latitude"] else { return nil }
guard let longitude = dictionary["longitude"] else { return nil }
self.name = name as! String
self.id = id as! Int
self.latitude = latitude as! Double
self.longitude = longitude as! Double
}
but I get the error:
Could not cast value of type 'NSNull' (0x10f387600) to 'NSNumber' (0x10f77f2a0).
like this:
I would think that the guard statement would prevent this. What am I missing?
You can cast metroLocations as an array of dictionaries, namely:
Array<Dictionary<String, String>>
Or, more concisely:
[[String: String]]
Thus:
if let dataFromNetworking = response.result.value {
guard let metroLocations = dataFromNetworking["metro_locations"] as? [[String: String]] else {
print("this was not an array of dictionaries where the values were all strings")
return
}
var locations = [Location]()
for dictionary in metroLocations {
if let location = Location(dictionary: dictionary) {
locations.append(location)
}
}
}
Where
class Location {
let name: String
init?(dictionary: [String: String]) {
guard let name = dictionary["name"] else { return nil }
self.name = name
}
}
Clearly, I used [[String: String]] to represent an array of dictionaries where the values were all strings, as in your example. If the values included objects other than strings (numbers, booleans, etc.), then you might use [[String: AnyObject]].
In your revision, you show us a more complete Location implementation. You should avoid as! forced casting, and instead us as? in the guard statements:
class Location {
let id: Int
let name: String
let latitude: Double
let longitude: Double
init?(dictionary: [String: AnyObject]) {
guard let id = dictionary["id"] as? Int,
let name = dictionary["name"] as? String,
let latitude = dictionary["latitude"] as? Double,
let longitude = dictionary["longitude"] as? Double else {
return nil
}
self.name = name
self.id = id
self.latitude = latitude
self.longitude = longitude
}
}