Swift - How to write complex objects to Firebase realtime database - ios

I'm new to the Firebase realtime database and relatively new to Swift in general. I am attempting to build a song request app in which users can create events for guests to request songs from the Spotify API. I'm trying to write an Event object to Firebase, which contains nested objects and arrays of different types. However, when it writes to the database, it only writes the strings and none of the arrays or objects. What is the best way to write all this information to the Firebase Database in a nested structure, so that whenever users add song requests, I can edit the array of requests for the given event in firebase.
Here is my code:
Event.swift
struct Event: Codable{
var code: String
var name: String
var host: String
var description: String
var hostUserId: String
var guestIds: [String]
var requests: [Request]
var queue: [Request]
var played: [Request]
//private var allowExplicit: Bool
//private var eventLocation
init(code: String, name: String, host: String, description: String, hostUserId: String){
self.code = code
self.name = name
self.host = host
self.description = description
self.hostUserId = hostUserId
self.guestIds = []
self.requests = []
self.queue = []
self.played = []
}
func toAnyObject() -> Any{
var guestIdsDict: [String:String] = [:]
for id in guestIds{
guestIdsDict[id] = id
}
var requestsDict: [String: Any] = [:]
for request in requests{
requestsDict[request.getId()] = request.toAnyObject()
}
var queueDict: [String: Any] = [:]
for request in queue{
queueDict[request.getId()] = request.toAnyObject()
}
var playedDict: [String: Any] = [:]
for request in played{
playedDict[request.getId()] = request.toAnyObject()
}
return [
"code": code,
"name": name,
"host": host,
"description": description,
"hostUserId": hostUserId,
"guestIds": guestIdsDict,
"requests": requestsDict,
"queue":queueDict,
"played":playedDict
]
}
}
Request.swift
struct Request: Codable{
private var name: String
private var id: String
private var explicit: Bool
private var album: Album
private var artists: [Artist]
private var likes: Int
init(name: String, id: String, explicit: Bool, album: Album, artists: [Artist]){
self.name = name
self.id = id
self.explicit = explicit
self.album = album
self.artists = artists
self.likes = 1
}
func toAnyObject() -> Any{
var artistsDict: [String:Any] = [:]
for artist in artists {
artistsDict[artist.id] = artist.toAnyObject()
}
return [
"name": name,
"id": id,
"explicit": explicit,
"album": album.toAnyObject(),
"artists": artistsDict,
"likes": likes
]
}
mutating func like(){
self.likes += 1
}
mutating func unlike(){
self.likes -= 1
if(self.likes < 0){
self.likes = 0
}
}
mutating func setLikes(count: Int){
self.likes = count
}
func getLikes() -> Int{
return self.likes
}
func getName() -> String{
return self.name
}
func getId() -> String{
return self.id
}
func getExplicit() -> Bool{
return self.explicit
}
func getAlbum() -> Album {
return self.album
}
func getImages() -> [Image] {
return self.album.images
}
func getArtists() -> [Artist] {
return self.artists
}
func getArtistString() -> String{
var artistString = ""
for (i, artist) in self.artists.enumerated(){
artistString += artist.name
if(i != self.artists.endIndex-1){
artistString += ", "
}
}
return artistString
}
}
Album.swift
struct Album: Codable{
let name: String
let images: [Image]
func toAnyObject() -> Any{
var imagesDict: [String: Any] = [:]
for image in images{
imagesDict[image.url] = image.toAnyObject()
}
return [
"name": name,
"images": imagesDict
]
}
}
Artist.swift
struct Artist: Codable{
let id: String
let name: String
func toAnyObject() -> Any{
return ["id": id, "name": name]
}
}
Image.swift
struct Image: Codable{
let height: Int
let url: String
let width: Int
func toAnyObject() -> Any{
return ["height": height, "url": url, "width": width]
}
}

As you are using Codable, you can create a dic out of it as follows:
Step 1: Add this extension to your code
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
Step 2: Write below code in your Struct (this you have to do in every struct or you can modify code as per your need).
func createDic() -> [String: Any]? {
guard let dic = self.dictionary else {
return nil
}
return dic
}
Now with the help of struct obj, call createDic() method and you will get a dictionary.
And you can send this dictionary to the firebase.
FULL CODE EXAMPLE:
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
}
struct LoginModel: Codable {
let email: String
let password: String
func createDic() -> [String: Any]? {
guard let dic = self.dictionary else {
return nil
}
return dic
}
}
Please comment if you have any questions.
Happy to help!

Related

Remove duplicate elements from object array

I am attempting to remove duplicate elements of my Transaction object. The Transactions are being loaded from Firestore and displayed onto a UTableView. I tried to follow this answer [here][1] however I got an error that budgetData is not hashable. Is there a way I can remove duplicate Transactions that have the same "transId" and return an updated array of budgetdata?
var budgetData: [Transaction] = []
func loadCatTransactions(){
if let catId = self.categoryId{
guard let user = Auth.auth().currentUser?.uid else { return }
print("userFromLoadChat::\(user)")
db.collection("users").document(user).collection("Transactions")
.whereField("catId", isEqualTo: catId)
.getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
self.budgetData.removeAll()
for document in snapshot!.documents {
let data = document.data()
let title = data["name"] as? String ?? ""
let date = data["date"] as? String ?? ""
let amount = data["amount"] as? Double ?? 0
let id = data["transId"] as? String ?? ""
let trans = Transaction(catId:catId,title: title, dateInfo: date, image: UIImage.groceriesIcon, amount: amount)
self.budgetData.append(trans)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
func uniq<S : Sequence, T : Hashable>(source: S) -> [T] where S.Iterator.Element == T {
var buffer = [T]()
var added = Set<T>()
for elem in source {
if !added.contains(elem) {
buffer.append(elem)
added.insert(elem)
}
}
return buffer
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.budgetData.count
}
struct Transaction {
var catId : String? = nil
var title: String
var dateInfo: String
var image: UIImage
var amount: Double
var annualPercentageRate: Double?
var trailingSubText: String?
var uid: String?
var outOfString: String?
var category: String?
var dictionary:[String:Any]{
return [
"title": title,
"dateInfo":dateInfo,
"amount":amount,
"annualPercentageRate":annualPercentageRate,
"trailingSubText":trailingSubText,
"uid": uid,
"outOfString": outOfString,
"category": category
]
}
}
[1]: Removing duplicate elements from an array in Swift
You need to make Transaction object Hashable. Try this
struct Transaction{
var transId: String
}
extension Transaction: Hashable{
static func ==(lhs: Transaction, rhs: Transaction) -> Bool {
return lhs.transId == rhs.transId
}
}
var budgetData = [Transaction(transId: "a"), Transaction(transId:"c"),
Transaction(transId: "a"), Transaction(transId: "d")]
var tranSet = Set<Transaction>()
budgetData = budgetData.filter { (transaction) -> Bool in
if !tranSet.contains(transaction){
tranSet.insert(transaction)
return true
}
return false
}

Retrieving custom Object from NSUserDefaults

I have a dictionary of values
class Objects {
let values = [
"AAA": ["AAAAAAA", "111111111"],
"BBB": ["BBBBBBBB", "2222222"],
"CCC": ["CCCCCCCC", "3333333333"],
"DDD": ["DDDDDD", "44444444"],
]
}
Which I turn into custom objects and display in a tableview.
struct Object {
var heading : String!
var imageName: String!
}
Then the user can select two objects to store in UserDefaults
let defaults = UserDefaults.standard
func addObject(_ object1: String, object2: String) {
// Get objects for user
var userObjects = fetchObjectsFromUserDefaults()
// Add to user currencies
userObjects.append([object1,object2])
//Update user defaults value for key
// [ [Object1, Object2], [Object1, Object2] ]
defaults.set(userObject, forKey: "userCurrencies")
}
// Gets [[String]] values from user defaults for key
func fetchObjectsFromUserDefaults() -> [[String]] {
if let objects = UserDefaults.standard.value(forKey: "userObjects") {
return objects as! [[String]]
} else {
return []
}
}
// Uses [[String]] values and turns them into objects by using the dictionary to determine property values
func getObject() -> [[Object]] {
let userObject = fetchObjectsFromUserDefaults()
// [ [Object1, Object2], [Object1, Object2] ]
let object = Object()
var fetchedObject = [[Object]]()
if !userObjects.isEmpty {
for c in userObjects {
var set = [Object]()
if let val = object.available[c[0]] {
set.append(Currency(currencyTitle: c[0], imageName: val[0] ))
}
if let val2 = object.available[c[1]] {
set.append(Currency(currencyTitle: c[0], imageName: val2[0] ))
}
if !set.isEmpty {
fetchedObjects.append(set)
}
}
return fetchedObjects
}
return [[]]
}
View Controller
Here I get the objects to load into the TableView
let fetched = dataManager.getObjects
print(fetched)
self.objects = fetched()
However this prints out
(Function)
What am I doing wrong and is their a better method of storing and retrieving this data from user defaults ? I feel this is over kill and there is a swifter and safer approach.
Step 1.
Make your struct Codable. The compiler will write all of the functions for you if all of the members of your struct are Codable and fortunately String is Codable so its just:
struct Object: Codable {
var heading : String!
var imageName: String!
}
Step 2.
The problem with Codable is that it converts to and from Data, but you want to convert to and from a Dictionary. Fortunately JSONSerialization converts from Data to Dictionary so make a new protocol and give it a default implementation with a protocol extension:
protocol JSONRepresentable {
init?(json: [String: Any])
func json() -> [String: Any]
}
extension JSONRepresentable where Self: Codable {
init?(json: [String:Any]) {
guard let value = (try? JSONSerialization.data(withJSONObject: json, options: []))
.flatMap ({ try? JSONDecoder().decode(Self.self, from: $0) }) else {
return nil
}
self = value
}
func json() -> [String:Any] {
return (try? JSONEncoder().encode(self))
.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [String: Any] ?? [:]
}
}
Step 3.
Conform your struct to JSONRepresentable
struct Object: Codable, JSONRepresentable {
var heading : String!
var imageName: String!
}
Step 4.
Place your object into Userdefaults and get it out again:
let o = Object.init(heading: "s", imageName: "a").json()
UserDefaults.standard.set(o, forKey: "test")
print(Object.init(json: UserDefaults.standard.dictionary(forKey: "test") ?? [:]))
Here is the whole playground if you want to try:
import UIKit
struct Object: Codable, JSONRepresentable {
var heading : String!
var imageName: String!
}
protocol JSONRepresentable {
init?(json: [String: Any])
func json() -> [String: Any]
}
extension JSONRepresentable where Self: Codable {
init?(json: [String:Any]) {
guard let value = (try? JSONSerialization.data(withJSONObject: json, options: []))
.flatMap ({ try? JSONDecoder().decode(Self.self, from: $0) }) else {
return nil
}
self = value
}
func json() -> [String:Any] {
return (try? JSONEncoder().encode(self))
.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [String: Any] ?? [:]
}
}
let o = Object.init(heading: "s", imageName: "a").json()
UserDefaults.standard.set(o, forKey: "test")
print(Object.init(json: UserDefaults.standard.dictionary(forKey: "test") ?? [:]))

How to store an array of objects in Firestore

I can't find online how to store an array of objects so that the key "line_items" presents numbers for each menuItem with values for each menuItem corresponding to its own number. In other words, I need the numbers to come after line_items rather than the nested key so that each individual MenuItem object can be quickly referenced. I found online how to make it so each key has an array of values, but I need line_items to have an array of MenuItem objects. The following code crashes:
public func uploadTransactionData(_ menuItems: [MenuItem], balanceId: String, subTotal: Int, completion: #escaping (() -> ())) {
guard let userId = Auth.auth().currentUser?.uid else { completion(); return }
let utilitiesManager = UtilitiesManager()
let timestamp = utilitiesManager.timestamp()
let params: [String: Any] = ["date": "\(timestamp)",
"balance_id": "\(balanceId)",
"subtotal": "\(subTotal)",
"user_id": "\(userId)",
"line_items": menuItems
]
Firestore.firestore().document("transaction_history/\(timestamp)").setData(params)
{ err in
if let e = err {
print("$-- error creating user \(e)")
completion()
} else {
completion()
}
}
}
Here's the MenuItem model:
struct MenuItem {
let itemId: String
let name: String
var modifiers: [String]?
var photoName: String?
var photoUrl: String?
var quantity: Int
var price: Int
var sizeAddOnPrice: Int
var toppingsAddOnPrice: Int
let description: String
var size: String
let category: String
init(itemId: String, name: String, modifiers: [String]?, photoName: String?, photoUrl: String?, quantity: Int, price: Int, sizeAddOnPrice: Int, toppingsAddOnPrice: Int, description: String, size: String, category: String) {
self.itemId = itemId
self.name = name
self.modifiers = modifiers
self.photoName = photoName
self.photoUrl = photoUrl
self.quantity = quantity
self.price = price
self.sizeAddOnPrice = sizeAddOnPrice
self.toppingsAddOnPrice = toppingsAddOnPrice
self.description = description
self.size = size
self.category = category
}
Problem:
Your app is crashing because you are trying to save user defined object MenuItem to Firestore. Firestore doesn't allow it. Firestore only supports this datatypes.
Solution:
You can convert your custom object MenuItem to Firestore supported datatypes.
You can do this by making following changes to your code.
Make MenuItem confirm to Codable protocol.
struct MenuItem: Codable {
// Your code as it is.
}
Make following changes to your uploadTransactionData() function:
public func uploadTransactionData(_ menuItems: [MenuItem], balanceId: String, subTotal: Int, completion: #escaping (() -> ())) {
let userId = Auth.auth().currentUser?.uid else { completion(); return }
let utilitiesManager = UtilitiesManager()
let timestamp = utilitiesManager.timestamp()
var list_menuItem = [Any]()
for item in menuItems {
do {
let jsonData = try JSONEncoder().encode(item)
let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
list_menuItem.append(jsonObject)
}
catch {
// handle error
}
}
let params: [String: Any] = ["date": "\(timestamp)",
"balance_id": "\(balanceId)",
"subtotal": "\(subTotal)",
"user_id": "\(userId)",
"line_items": list_menuItem
]
Firestore.firestore().document("transaction_history/\(timestamp)").setData(params)
{ err in
if let e = err {
print("$-- error creating user \(e)")
completion()
} else {
completion()
}
}
}
This is because Firestore doesn't know how to save the value: menuItems
you can map it like this: "objectExample": [
"a": 5,
"b": [
"nested": "foo"
]
]
or:
.setData([
"name": "Frank",
"favorites": [ "food": "Pizza", "color": "Blue", "subject": "recess" ],
"age": 12
])

Realm filter for both parent and child classes

I want to implement filter on both the parent and child, as if search 'chicken2' result should return only lines with meal as 'chicken2' + meals with name 'chicken2', below are my model classes with query and result.
import Foundation
import RealmSwift
class Canteen: Object {
#objc dynamic var name: String?
let lines = List<Line>()
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
let lines = json["lines"] as! [[String: Any]]
for lineJSON in lines {
let line = Line()
line.initWithJSON(json: lineJSON)
self.lines.append(line)
}
}
override static func primaryKey() -> String? {
return "name"
}
}
class Line: Object {
#objc dynamic var name: String?
var meals = List<Meal>()
let canteens = LinkingObjects(fromType: Canteen.self, property: "lines")
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
let meals = json["meals"] as! [[String: Any]]
for mealJSON in meals {
let meal = Meal()
meal.initWithJSON(json: mealJSON)
self.meals.append(meal)
}
}
override static func primaryKey() -> String? {
return "name"
}
}
class Meal: Object {
#objc dynamic var name: String?
#objc dynamic var vegan: Bool = false
let lines = LinkingObjects(fromType: Line.self, property: "meals")
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
self.vegan = json["isVegan"] as! Bool
}
override static func primaryKey() -> String? {
return "name"
}
}
Below is my controller class's viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
let file = Bundle.main.path(forResource: "mealss", ofType: ".json")
let data = try! Data(contentsOf: URL(fileURLWithPath: file!))
let json = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
if let dict = json as? [String: Any] {
let canteen = Canteen()
canteen.initWithJSON(json: dict)
try! realm.write {
realm.add(canteen, update: true)
}
}
realm.objects(Line.self).filter("ANY meals.name contains 'chicken2'")
print(lines.description)
}
below is the output of my print statement.
Below is the json file which i have used.
{
"name": "canteen1",
"lines": [
{
"name": "line1",
"meals": [
{
"name": "chicken2",
"isVegan": false
},
{
"name": "egges",
"isVegan": false
}
]
},
{
"name": "line2",
"meals": [
{
"name": "chicken",
"isVegan": true
},
{
"name": "egges",
"isVegan": true
}
]
}
]
}
Below is my expected output.
[Line {
name = line1;
meals = List<Meal> <0x281301b90> (
[0] Meal {
name = chicken2;
vegan = 0;
}
);
}]
You can retrieve a Meal object and show its parent object's name if you change the Meal class like this.
class Meal: Object {
#objc dynamic var name: String?
#objc dynamic var vegan: Bool = false
let lines = LinkingObjects(fromType: Line.self, property: "meals")
var line: Line { return lines.first! } // <- added
func initWithJSON(json: [String: Any]) {
self.name = json["name"] as? String
self.vegan = json["isVegan"] as! Bool
}
override static func primaryKey() -> String? {
return "name"
}
}
Instead of Line objects, retrieve Meal objects and access their parent object's name.
let meals = realm.objects(Meal.self).filter("name contains 'chicken2'")
for meal in meals {
print("name = \(meal.line.name!)")
print(meal)
}
Here is the output:
name = line1
Meal {
name = chicken2;
vegan = 0;
}

Call func with various struct

I want to create one func which i can used with various struct.
I have several struct and I want use one func with all my struct.
I work with Firestore and want use this one func to access the Firestore.
My first struct:
struct Profile {
var name = ""
var surname = ""
var email = ""
var dictionary: [String: Any] {
return [
"name": name,
"surname": surname,
"email": email
]
}
}
extension Profile: DocumentSerializable {
init?(dictionary: [String: Any], id: String) {
let name = dictionary["name"] as? String ?? ""
let surname = dictionary["surname"] as? String ?? ""
let email = dictionary["email"] as? String ?? ""
self.init(name: name,
surname: surname,
email: email)
}
}
My second struct:
struct FavoriteList {
var favoriteList: [String]
var id: String
var dictionary: [String: Any] {
return [
"favoriteList": favoriteList,
"id": id
]
}
}
extension FavoriteList: DocumentSerializable {
init?(dictionary: [String : Any], id: String) {
let favoriteList = dictionary["favorite"] as? [String] ?? [""]
let id = id
self.init(favoriteList: favoriteList, id: id)
}
}
And my func which I used now to load data from firestore:
func observeQuery() {
guard let query = query else { return }
let time = DispatchTime.now() + 0.5
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
if let snapshot = snapshot {
DispatchQueue.main.asyncAfter(deadline: time) {
let profileModels = snapshot.documents.map { (document) -> Profile in
if let profileModel = Profile(dictionary: document.data(), id: document.documentID) {
return profileModel
} else {
fatalError("Error!")
}
}
self.profile = profileModels
self.document = snapshot.documents
self.tableView.reloadData()
}
}
}
}
So how I can make func observeQuery to use my structs Profile or FavouriteList?
You can use Generic Functions :
func observeQuery<T>(someObject: T) {
if someObject is Profile {
//do something
} else if someObject is FavouriteList {
//do something
}
}

Resources