Get Firebase document objects as Swift object - ios

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
}
}
}

Related

How to convert document to a custom object in Swift 5?

I've been trying to convert the document retrieved from the Firebase's Cloud Firestore to a custom object in Swift 5. I'm following the documentation:
https://firebase.google.com/docs/firestore/query-data/get-data#custom_objects
However, Xcode shows me the error Value of type 'NSObject' has no member 'data' for the line try $0.data(as: JStoreUser.self). I've defined the struct as Codable.
The code:
func getJStoreUserFromDB() {
db = Firestore.firestore()
let user = Auth.auth().currentUser
db.collection("users").document((user?.email)!).getDocument() {
(document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: JStoreUser.self)
}
}
}
}
The user struct:
public struct JStoreUser: Codable {
let fullName: String
let whatsApp: Bool
let phoneNumber: String
let email: String
let creationDate: Date?
}
The screenshot:
Does anyone know how to resolve this?
After contacting the firebase team, I found the solution I was looking for. It turns out I have to do import FirebaseFirestoreSwift explicitly instead of just doing import Firebase. The error will disappear after this. (And of course you'll need to add the pod to your podfile first:D)
You can do it as shown below:-
First create model class:-
import FirebaseFirestore
import Firebase
//#Mark:- Users model
struct CommentResponseModel {
var createdAt : Date?
var commentDescription : String?
var documentId : String?
var dictionary : [String:Any] {
return [
"createdAt": createdAt ?? "",
"commentDescription": commentDescription ?? ""
]
}
init(snapshot: QueryDocumentSnapshot) {
documentId = snapshot.documentID
var snapshotValue = snapshot.data()
createdAt = snapshotValue["createdAt"] as? Date
commentDescription = snapshotValue["commentDescription"] as? String
}
}
Then you can convert firestore document into custom object as shown below:-
func getJStoreUserFromDB() {
db = Firestore.firestore()
let user = Auth.auth().currentUser
db.collection("users").document((user?.email)!).getDocument() { (document, error) in
// Convert firestore document your custom object
let commentItem = CommentResponseModel(snapshot: document)
}
}
You need to initialize your struct and then you can extend the QueryDocumentSnapshot and QuerySnapshot like:
extension QueryDocumentSnapshot {
func toObject<T: Decodable>() throws -> T {
let jsonData = try JSONSerialization.data(withJSONObject: data(), options: [])
let object = try JSONDecoder().decode(T.self, from: jsonData)
return object
}
}
extension QuerySnapshot {
func toObject<T: Decodable>() throws -> [T] {
let objects: [T] = try documents.map({ try $0.toObject() })
return objects
}
}
Then, try to call the Firestore db by:
db.collection("users").document((user?.email)!).getDocument() { (document, error) in
guard error == nil else { return }
guard let commentItem: [CommentResponseModel] = try? document.toObject() else { return }
// then continue with your code
}
In the past, I had some issues though importing FirebaseFirestore with the package manager in my project.
So I explain about the access to FirebaseFirestore in swift.
SnapshotListener
import Foundation
import FirebaseFirestore
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.map { queryDocumentSnapshot -> Book in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let author = data["author"] as? String ?? ""
let numberOfPages = data["pages"] as? Int ?? 0
return Book(id: .init(), title: title, author: author, numberOfPages: numberOfPages)
}
}
}
}
using uid and getDocument function
Firestore.firestore().collection("users").document(uid).getDocument { snapshot, error in
if let error = error {
self.errorMessage = "Failed to fetch current user: \(error)"
print("Failed to fetch current user:", error)
return
}
guard let data = snapshot?.data() else {
self.errorMessage = "No data found"
return
}
let uid = data["uid"] as? String ?? ""
let email = data["email"] as? String ?? ""
let profileImageUrl = data["profileImageUrl"] as? String ?? ""
self.chatUser = ChatUser(uid: uid, email: email, profileImageUrl: profileImageUrl)
}

Swift Compact Map returning empty

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.

REST API call using Swift to recreate App Store

So I have two models, one called AppCategory and App. I've created a function within the AppCategory class that makes an API call that brings me back all categories successfully and within those categories there is an array of dictionaries of apps which I get back in form of dictionaries but i'm not sure on how to set it as an App object.
Here is my AppCategory class:
import UIKit
class AppCategory {
var name: String?
var apps = [App]()
var type: String?
init(dictionary: [String: Any]) {
name = dictionary["name"] as? String
apps = dictionary["apps"] as! [App]
type = dictionary["type"] as? String
}
static func fetchFeaturedApps(completion: #escaping ([AppCategory]) -> Void) {
guard let url = URL(string: "https://api.letsbuildthatapp.com/appstore/featured") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
return
}
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
var appCategories = [AppCategory]()
for dict in json["categories"] as! [[String: Any]] {
let appCategory = AppCategory(dictionary: dict)
appCategories.append(appCategory)
if dict.index(forKey: "apps") != nil {
}
}
print(appCategories)
DispatchQueue.main.async {
completion(appCategories)
}
} catch {
print("Error in JSON Serialization")
}
}.resume()
}
}
Here is the App model class:
import Foundation
class App {
var id: Int?
var name: String?
var category: String?
var price: Double?
var imageName: String?
init(dictionary: [String: Any]) {
id = dictionary["Id"] as? Int
name = dictionary["Name"] as? String
category = dictionary["Category"] as? String
price = dictionary["Price"] as? Double
imageName = dictionary["ImageName"] as? String
}
}
I've done research and I could make these classes of type NSObject and use the setValue(forKey:) but I don't really want to do that. I've gotten as far as what's highlighted in the AppCategory class by saying if dict.index(forKey: "apps") != nil... Don't know if i'm on the right track but maybe someone can help me with this.

How do I create a custom struct to assign the contents of array into usable variables?

I have a JSON function that previously mapped the contents of an array (countries, divisions, teams (not shown)) into seperate variables using this code:
let task = URLSession.shared.dataTask{ (response: URLResponse?, error: Error?) in
DispatchQueue.main.async
{
if error != nil {
print("error=\(error)")
return
}
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:AnyObject]
print (json)
if let arr = json?["countries"] as? [[String:String]] {
self.countryId = arr.flatMap { $0["id"]!}
self.countries = arr.flatMap { $0["name"]!}
self.teamsTableView.reloadData()
print ("Countries: ",self.countries)
}
if let arr = json?["divisions"] as? [[String:String?]] {
self.divisions = arr.flatMap { ($0["name"]! )}
self.divisionId = arr.flatMap { ($0["id"]! )}
self.teamsTableView.reloadData()
print ("Divisions: ",self.divisions)
}
} catch{
print(error)
}
}
}
However, I have since learned "that mapping multiple arrays as data source in conjunction with flatMap is pretty error-prone", and that I should use a custom struct instead.
How / Where do I begin to write a custom struct to assign the contents of this JSON array into seperate variables?
Thanks
UPDATE:
I have used the code as suggested below and I believe this is along the right lines. Below is the current output.
Countries: [KeepScore.SumbitScoreViewController.Country(name: Optional("Denmark"), countryId: Optional("1")), KeepScore.SumbitScoreViewController.Country(name: Optional("Belgium"), countryId: Optional("2")), KeepScore.SumbitScoreViewController.Country(name: Optional("Brasil"), countryId: Optional("3")),
However by the looks of it, it is also putting the countryId into the variable and I'd like just the name so I could call it in a UITable...What are the next steps in doing this?
Same idea as you were doing, except instead of mapping each individual property you map the whole country.
//define a struct
struct Country {
var name: String?
var countryId: String?
init(_ dictionary: [String : String]) {
self.name = dictionary["name"]
self.countryId = dictionary["id"]
}
}
//defined in your class
var countries = [Country]()
//when parsing your JSON
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:AnyObject]
print (json)
if let arr = json?["countries"] as? [[String : String]] {
self.countries = arr.flatMap { Country($0) }
print ("Countries: ",self.countries)
}
Apply the same idea to divisions.
Edit:
In the code you've posted, you extract both the name and the id, that's why I included both in the struct. So you can remove the id from the struct if you wish, or add other variables in the future. Then when you want to extract them and use them in your table view you just do:
let country = countries[indexPath.row]
cell.textLabel.text = country.name

Firebase Posting Dictionary in iOS Swift - Can't get get data to load in Tableview

I am posting to Firebase using a Post Dictionary. See code below:
class Post {
let eventImageURL: String?
let postID: String?
static var feed:[Post]?
init(id: String?, eventImageURL: String?)
{
self.postID = id
self.eventImageURL = eventImageURL
}
static func initWithPostID(postID: String, postDict: [String: String]) -> Post? {
guard let eventImageURL = postDict["eventImageURL"] else {
// Conditiions if failed...
print("Invalid Post Dictionary")
return nil
}
return Post(id: postID, eventImageURL: eventImageURL)
}
func dictValue() -> [String: String] {
var postDict = [String: String]()
postDict["eventImageURL"] = eventImageURL
return postDict
}
}
I am then trying to call each post into a TableView using this code:
override func viewDidLoad() {
super.viewDidLoad()
postRef.observe(.value, with: { snapshot in
guard let posts = snapshot.value as? [String: [String: String]] else {
print("Error")
return
}
Post.feed?.removeAll()
for (postID, post) in posts {
let newPost = Post.initWithPostID(postID: postID, postDict: post)!
Post.feed?.append(newPost)
}
Post.feed = Post.feed?.reversed()
self.tableView.reloadData()
}, withCancel : { (error) in
print("Error \(error.localizedDescription)")
})
}
When I call Post.feed.count it returns zero (i.e. empty string). What am I missing? It also means I can't view any data in TableView.
The Post Dictionary works when posting to Firebase, a typical post looks like this:
["-KRTbOfj2vswyFntPdnp": ["eventImageURL": "https://firebasestorage.googleapis.com/v0/b.co/appspotm/o/EventImages%2F-KRTbFz1lpnACmQucPLQ%2FeventImage.jpg?alt=media&token=b013fd9f-cf36-4139-a8cc-afa5f1daa8b3"]]

Resources