I am working on consuming the restful web service which returns the data in JSON format. This is the Sample data :
[
{
"name": "Mark E",
"categories": "process",
"id": 1,
"checkedOut": null,
"checkedOutBy": null
},
{
"name": "John",
"categories": null,
"id": 2,
"checkedOut": null,
"checkedOutBy": null
}
]
I am parsing this json using this code. Also I created struct model for this.
let task = session.dataTask(with: url) { (data, response, error) in
var myModel = [MyModel]()
if let HTTPResponse = response as? HTTPURLResponse {
let status = HTTPResponse.statusCode
if(status == 200) {
guard let data = data else {
print("No Data!!")
return completion(false, myModel)
}
guard let json = try! JSONSerialization.jsonObject(with: data, options: []) as? NSArray else {
print("Not an array")
return completion(false, myModel)
}
for jsondata in json {
guard let newdata = MyModel(json: jsondata) else {
continue
}
myModel.append(newdata)
}
completion(true,myModel)
}else {
completion(false,myModel)
}
}
}
task.resume()
And this is my data model struct
struct MyModel {
var name : String
var categories : String
var id : Int
var checkedOut : String
var checkedOutBy : String
init?(json:Any) {
guard let myModel = json as? NSDictionary else {
return nil
}
guard let name = myModel["name"] as? String,
let id = myModel["id"] as? Int else {
return nil
}
self.name = author
self.id = id
// This is how I am handling the null values.
if let categories = myModel["categories"] as? String {
self.categories = categories
} else {
self.categories = ""
}
if let lastCheckedOut = myModel["lastCheckedOut"] as? String {
self.lastCheckedOut = lastCheckedOut
} else {
self.lastCheckedOut = ""
}
if let lastCheckedOutBy = myModel["lastCheckedOutBy"] as? String {
self.lastCheckedOutBy = lastCheckedOutBy
}else {
self.lastCheckedOutBy = ""
}
}
}
Here in struct MyModel, I am using if let for checking null values. Could anyone please suggest me this is the right way to check each variable for null? Is there any other way to do this?
If I check for null value using guard, its not adding any object into array.
If the value is null then it simply assign empty char "" to that variable and it will add into MyModel array of objects.
The usual way to check for a specific type and assign something else on failure is the nil coalescing operator:
self.lastCheckedOut = myModel["lastCheckedOut"] as? String ?? ""
Side-note: Consider that JSON null is deserialized to NSNull rather than to nil
I would advise on a couple of approaches:
Setting MyModel struct properties as optionals. In this way, you don't need to use if let or guard statements.
I can recommend ObjectMapper, a Swift 3 pods which maps JSON very efficiently. For example:
Instead of assigning:
If let _value = dictionary["value"] as? String { self.value = _value
}
You will use
self.value = map["value"]
ObjectMapper
Wrote this on my phone, untested ;-)...
Related
I created a struct UserModel.swift to store the json data:-
struct UserModel {
var id: Int = 0
var uuid:Int = 0
var user_name: String = ""
var password: String = ""
var name: String = ""
var email: String = ""
init(json: [String:Any]) {
if let obj = json["id"] as? Int {
self.id = obj
}
if let obj = json["uuid"] as? Int {
self.uuid = obj
}
if let obj = json["user_name"] as? String {
self.user_name = obj
}
if let obj = json["name"] as? String {
self.name = obj
}
if let obj = json["email"] as? String {
self.email = obj
}
}
}
Now I used Alamofire to get the json in my ViewController.Swift file and I stored my struct by creating a variable I am successfully get the json and stored it in my struct.
var userModel = [UserModel]()
private func getList() {
progressHUD.show(in: view, animated: true)
// let uuid = UserDefaults.standard.integer(forKey: "uuid")
Alamofire.request(Constants.API.url("list_request?device_token=\(device_token ?? "")&uuid=794849"), method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil).responseJSON {
(response:DataResponse<Any>) in
self.progressHUD.dismiss()
guard let json = response.result.value as? [String:Any] else {return}
guard let data = json["data"] as? [[String:Any]] else {return}
printD(data)
guard let status = json["status"] as? Bool else { return}
printD(status)
if status == true {
guard let data = json["data"] as? [[String:Any]] else {return}
for userData in data {
self.userModel.append(UserModel(json: userData))
printD(self.userModel)
}
CommonClass.shared.showSuccessMessage("\(json["msg"] as? String ?? "")", inViewController: self)
}
else if status == false {
CommonClass.shared.showErrorMessage("\(json["msg"] as? String ?? "")", inViewController: self)
}
else {
CommonClass.shared.showErrorMessage("Server Connection Error. Please try again later", inViewController: self)
}
self.tableView.reloadData()
}
}
Now the problem I am getting I have to use my userModel data in another api in same class and I don't know How Can I do that. In rejectRequest() function I need to access some of my struct data but I don't know how can I do that. Please help?
private func rejectRequest() {
let user = userModel // I need to use userModel data for parameter
let param: [String:Any] = ["from_user": "", "to_user": "", "request_id": "", "device_token": device_token ?? ""]
Alamofire.request(Constants.API.url("end_request"), method: .post, parameters: param, encoding: JSONEncoding.default, headers: nil).responseJSON {
(response:DataResponse<Any>) in
}
}
Create a struct to hold common data that is shared across the app. Here's a sample for a DataController that can hold different models or arrays / dicts of models:
struct DataController {
// MARK: - User management
struct Users {
static var users: [User] = []
static var currentUser: User = User()
static func addUser(_ user: User) {
users.append(user)
}
static func findUserById(_ id: Int) -> User? {
// Find the users that match the id (should only be one!)
let users = user.filter { $0.id = id }
return users.count == 1 ? users[0] : nil
}
}
}
Add a user:
DataController.Users.addUser(user)
Access from elsewhere by:
DataController.Users.currentUser
Find a user by an id:
if let user = DataController.Users.findUserById(id) {
... do stuff ...
}
Incidentally, might be simpler to use a guard statement when parsing your JSON data:
init(json: [String:Any]) {
guard
let id = json["id"] as? Int,
let uuid = json["uuid"] as? Int,
let user_name = json["user_name"] as? String,
let name = json["name"] as? String,
let email = json["email"] as? String
else {
return // Fail
}
self.id = id
self.uuid = uuid
self.user_name = user_name
self.name = name
self.email = email
}
I need to create an array of Categories that contains Questions array.
struct CategoryFB {
var title: String
var id: Int
var questions: [QuestionsFB]
var dictionary: [String : Any] {
return ["title" : title,
"id" : id]
}
}
extension CategoryFB {
init?(dictionary: [String : Any], questions: [QuestionsFB]) {
guard let title = dictionary["title"] as? String, let id = dictionary["id"] as? Int else { return nil }
self.init(title: title, id: id, questions: questions)
}
}
Firestore has a following structure
Collection("Categories")
Document("some_id")
Collection("Questions")
How to create array like this?
array = [Category(title: "First",
questions: [
Question("1"),
...
]),
... ]
My try was wrong:
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data(), questions: questionsArray!)})
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Your main problem seems to stem from the fact that you're regenerating your categories array every time you run your subquery, and when you do, you're only supplying a single questions array to the entire thing.
There's lots of ways to fix this. I would probably break it up so that you a) First allow yourself to create a category array without any questions, and then b) Go back through each of your individual subQueries and insert them into your categories as you get them.
Your final code might look something like this. Note that this would mean changing your Category object so that you can first create it without a Questions array, and implementing this custom addQuestions:toCategory: method (which would be a whole lot easier if you stored your categories as a dictionary instead of an array)
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data()})
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.addQuestions(questionsArray toCategory: document.documentID)
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Alternately, if you think you're going to be in a situation where you're always going to want to grab your questions every time you want to grab a category, you might consider not putting them in a subcollection at all, and just making them a map in the original category document.
This is the solution which I found by myself. Hopefully this will help someone in the future.
func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = db.collection("Categories")
var data = [Any]()
rootCollection.order(by: "id", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
for category in topSnapshot {
rootCollection.document(category.documentID).collection("Questions").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var questions = [Question]()
for document in snapshot {
let title = document.data()["title"] as! String
let details = document.data()["details"] as! String
let article = document.data()["article"] as! String
let link = document.data()["link"] as! String
let id = document.data()["id"] as! String
let possibleAnswers = document.data()["possibleAnswers"] as! [String]
let rightAnswerID = document.data()["rightAnswerID"] as! Int
let newQuestion = Question(title: title, article: article, details: details, link: link, possibleAnswers: possibleAnswers, rightAnswerID: rightAnswerID, id: id)
questions.append(newQuestion)
}
let categoryTitle = category.data()["title"] as! String
let collectionID = category.data()["id"] as! Int
let newCategory = Category(title: categoryTitle, id: collectionID, questions: questions)
data.append(newCategory)
//Return data on completion
completion(data)
})
}
}
})
}
Hi I am trying to learn RXSwift and First time I came across these concepts like Maps and Compact Maps.
I am able to get the response, but this line always returns empty.
objects.compactMap(DummyUser.init)
fileprivate let Users = Variable<[DummyUser]>([])
fileprivate let bag = DisposeBag()
response
.filter { response, _ in
return 200..<300 ~= response.statusCode
}
.map { _, data -> [[String: Any]] in
guard (try? JSONSerialization.jsonObject(with: data, options: [])) != nil else {
return []
}
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : Any]
// print(json!["results"])
return json!["results"] as! [[String : Any]]
}
.filter { objects in
return objects.count > 0
}
.map { objects in
// objects.forEach{print($0["name"]!)}
let names = objects.map { $0["name"]!}
print(names)
return objects.compactMap(DummyUser.init)
}
.subscribe(onNext: { [weak self] newEvents in
self?.processEvents(newEvents)
})
.disposed(by: bag)
func processEvents(_ newEvents: [DummyUser]) {
var updatedEvents = newEvents + Users.value
if updatedEvents.count > 50 {
updatedEvents = Array<DummyUser>(updatedEvents.prefix(upTo: 50))
}
Users.value = updatedEvents
DispatchQueue.main.async {
self.MianUsertable.reloadData()
}
// refreshControl?.endRefreshing()
let eventsArray = updatedEvents.map{ $0.dictionary } as NSArray
eventsArray.write(to: userFileURL, atomically: true)
}
My Json Response is Here
https://randomuser.me/api/?results=5
DummyUser Class
import Foundation
typealias AnyDict = [String: Any]
class DummyUser {
let gender: String
let name: AnyDict
let dob: String
let picture: AnyDict
init?(dictionary: AnyDict) {
guard let Dgender = dictionary["gender"] as? String,
let Dname = dictionary["name"] as? AnyDict,
let birthdata = dictionary["dob"] as? AnyDict,
let Ddob = birthdata["dob"] as? String,
let Dpicture = dictionary["picture"] as? AnyDict
else {
return nil
}
gender = Dgender
name = Dname
dob = Ddob
picture = Dpicture
}
var dictionary: AnyDict {
return [
"user": ["name" : name, "gender": gender, "dob": dob],
"picture" : ["userImage": picture]
]
}
}
In your DummyUser model you are using failable initializer, so in case of wrong dictionary provided to init method it will return nil.
compactMap automatically automatically filters nil's and that's the reason why your output is empty.
Looking at this piece of code:
let names = objects.map { $0["name"]!}
return objects.compactMap(DummyUser.init)
I would debug this variable called names because it probably has wrong input for the DummyUser initializer. It should be dictionary containing all of your DummyUser parameters. You can also debug your failable initializer to see which of the parameter is missing.
I try to implement a simple shopping list swift application for iOS as a personal project. I did follow a guide for iOS on youtube.
My question is how do I parse the Item object from firebase to my ShoppingListItem swift object? If I execute the following code, it doesn't show any error message but it does not show any results either. If I uncomment all "items" lines, it shows the expected results without the item information.
Here is a screenshot from the firebase console of my firebase firestore structure / example object
Thanks in advance!
ShoppingListItem.swift
import Foundation
import FirebaseFirestore
protocol DocumentSerializable {
init?(dictionary: [String: Any])
}
struct ShoppingListItem {
var shoppingItemID: String
var priority: Int
var quantity: Int
var item: Item
var dictionary: [String: Any] {
return [
"shoppingItemID": shoppingItemID,
"priority": priority,
"quantity": quantity,
"item": item,
]
}
}
extension ShoppingListItem: DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let shoppingItemID = dictionary["shoppingItemID"] as? String,
let priority = dictionary["priority"] as? Int,
let quantity = dictionary["quantity"] as? Int,
let item = dictionary["item"] as? Item
else { return nil }
self.init(shoppingItemID: shoppingItemID, priority: priority, quantity: quantity, item: item)
}
}
struct Item {
var itemID: String
var lastPurchase: String
var name: String
var note: String
var picturePath: String
var dictionary: [String: Any] {
return [
"itemID": itemID,
"lastPurchase": lastPurchase,
"name": name,
"note": note,
"picturePath": picturePath,
]
}
}
extension Item: DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let itemID = dictionary["itemID"] as? String,
let lastPurchase = dictionary["lastPurchase"] as? String,
let name = dictionary["name"] as? String,
let note = dictionary["note"] as? String,
let picturePath = dictionary["picturePath"] as? String else { return nil }
self.init(itemID: itemID, lastPurchase: lastPurchase, name: name, note: note, picturePath: picturePath)
}
}
Get Data call in TableViewController.swift
db.collection("shoppingList").getDocuments(){
querySnapshot, error in
if let error = error {
print("error loading documents \(error.localizedDescription)")
} else{
self.shoppingArray = querySnapshot!.documents.flatMap({ShoppingListItem(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
I used the Codable protocol.
I used this as an extension to the Encodable Protocol:
extension Encodable {
/// Returns a JSON dictionary, with choice of minimal information
func getDictionary() -> [String: Any]? {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any]
}
}
}
Then I use this to decode:
extension Decodable {
/// Initialize from JSON Dictionary. Return nil on failure
init?(dictionary value: [String:Any]){
guard JSONSerialization.isValidJSONObject(value) else { return nil }
guard let jsonData = try? JSONSerialization.data(withJSONObject: value, options: []) else { return nil }
guard let newValue = try? JSONDecoder().decode(Self.self, from: jsonData) else { return nil }
self = newValue
}
}
Make your two structs conform to Codable (Item first, then ShoppingListItem). Of course, this may not work for the existing data stored in Firestore. I would first put data into Firestore via the getDictionary() (in a new collection), then try to read it back into your tableView.
You may also want to print the actual error when trying to Decode your data, this will greatly help you pinpoint the data error if there's any.
extension Decodable {
/// Initialize from JSON Dictionary. Return nil on failure
init?(dictionary value: [String:Any]) {
guard JSONSerialization.isValidJSONObject(value) else {
return nil
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
let newValue = try JSONDecoder().decode(Self.self, from: jsonData)
self = newValue
}
catch {
log.error("failed to serialize data: \(error)")
return nil
}
}
}
I'm trying to create a dictionary from 2 json values, so far I can get one and append to an array but instead I would like to combine 2 values as a dictionary and add them to an array. This is what my function currently looks like:
var menuButtonsArray: [String?] = []
func getAppMenuButtons() {
guard let appJSON = appMenuJSON else {return}
guard let items = appJSON["items"] as? [[String: Any]] else {
return
}
let filteredItem = items.filter{$0["itemId"] as! String == "Items"}[0]
if let data = filteredItem["data"] as? [[String:Any]] {
for embeddedDict in data {
for (key, value) in embeddedDict {
if key == "name" {
menuButtonsArray.append(value as! String)
print("key: \(value)")
}
}
}
}
}
This is what the json looks like, it's shortened to fit this post:
"itemId": "Items",
"data": [
{
"name": "Item1",
"url": "www.item1.com"
},
{
"name": "Item2",
"url": "www.item2.com"
},
{
"name": "Item2",
"url": "www.item2.com"
}
]
What I'm trying to do is create an array of dictionaries with the format:
name:url
so something like :
["item1":"www.item1.com","item2":"www.item2.com", "item3":"www.item3.com"]
Here is an example on how you can use map() get the array you want:
var data = [["name": "Item1","url": "www.item1.com"],
["name": "Item2","url": "www.item2.com"],
["name": "Item3","url": "www.item3.com"]]
var newData = data.map { (dict: Dictionary) -> Dictionary<String, String> in
return [dict["name"]!:dict["url"]!] }
This code should give you
[ ["Item1": "www.item1.com"], ["Item2": "www.item2.com"], ["Item3":
"www.item3.com"] ]
as output
First of all if you need a Dictionary then declare your result as Dictionary instead of Array, you can use subscribe to use your embeddedDict["name"] as key in your resultDictionary and your embeddedDict["url"] as your resultDictionary value
func getAppMenuButtons() {
guard let appJSON = appMenuJSON else {return}
guard let items = appJSON["items"] as? [[String: Any]] else {
return
}
let filteredItem = items.filter{$0["itemId"] as! String == "Items"}[0]
var resultDict : [String:String] = [:]
if let data = filteredItem["data"] as? [[String:Any]] {
guard embeddedDict["name"] != nil && embeddedDict["url"] else{
continue
}
for embeddedDict in data {
resultDict[embeddedDict["name"]] = embeddedDict["url"]
}
}
debugPrint(resultDict) //in resultDict you will have your desired result
}