i have a FireBase database, inside i have a table of products and another table of orders with ids of these products, what i am trying to do is to get products from table of products based on ids inside table of orders, since FireBase will only allow me to get the products one by one , my tableview is loaded before i get all products that are referenced inside the orders table.
heres how i did that :
struct Product: Decodable, Encodable{
var id: String?
var ref: String
var description: String
var type: String
var price: String
var qtyOrdred:Int?
}
struct Order:Decodable, Encodable {
var id: String?
var isValide: Bool
var madeBy: String
var info: String?
var ordredProd: [OrderedProduct]
}
struct OrderedProduct:Decodable, Encodable {
var id: String
var qty: Int
}
func getData(completion: #escaping ([Product])->Void){
var allProduct = [Product]()
for product in orderedProduct {
getProductWithKey(qty: product.qty, key: product.id) { (p) in
print(p.ref)
allProduct.append(p)
}
}
}
func getProductWithKey(qty: Int,key: String, completion: #escaping (Product)->Void) {
Database.database().reference().child("products").child(key).observeSingleEvent(of: .value) { (snap) in
if let productObject = snap.value as? [String: Any]
{
if let ref = productObject["ref"],
let price = productObject["price"],
let type = productObject["type"],
let description = productObject["description"],
let id = productObject["id"]{
let p = Product(id: id as? String, ref: ref as! String, description: description as! String, type: type as! String, price: price as! String, qtyOrdred: qty)
completion(p)
}
}
}
}
i call it like this :
override func viewWillAppear(_ animated: Bool) {
self.getData { (ps) in
print(ps)
self.tableView.reloadData()
}
}
The problem is that it always print an empty array, and my tableview data never changes
You don't return from getData completion , you need a dispatch group
let g = DispatchGroup()
func getData(completion: #escaping ([Product])->Void){
var allProduct = [Product]()
for product in orderedProduct {
g.enter()
getProductWithKey(qty: product.qty, key: product.id) { (p) in
print(p.ref)
allProduct.append(p)
g.leave()
}
}
g.notify(queue:.main) {
completion(allProduct)
}
}
Your getData function is returning as soon as the for loop is finished. As the call inside the loop is async there isn't any data when the loop finishes.
Instead of reloading the table just insert rows as they arrive.
for product in orderedProduct {
getProductWithKey(qty: product.qty, key: product.id) { [weak self] (p) in
guard let self = self else { return }
allProduct.append(p)
guard let index = allProduct.firstIndex(of: p) else { return }
self.tableView.insertRow(at: IndexPath(row: index, section: 0))
}
}
Related
I create a fitness app and I use Realm as local database. During first launch I want to replace default realm with realm file which contains initial data (names of exercises, equipment, muscles engaged etc.). This initial data won't change in future. I wonder if exists some way which can help me to create reference in main class to another smaller classes. I need this to make filtering and getting data easier.
It's my main realm class
class Exercise: Object {
#Persisted var exerciseID: Int = 0
#Persisted var name: String = ""
#Persisted var category: Int
#Persisted var equipment: String
#Persisted var instruction: String
#Persisted var muscle: String
#Persisted var gif: String?
#Persisted var image: String? = nil
convenience init(name: String, category: Int, equipment: String, instruction: String, muscle: String, gif: String?, image: String?) {
self.init()
self.name = name
self.category = category
self.equipment = equipment
self.instruction = instruction
self.muscle = muscle
self.gif = gif
self.image = image
}
override static func primaryKey() -> String? {
return "exerciseID"
}
}
When I want to get all exercises and assigned equipment and muscles it is really a lot of code to retrieve this data especially when string contains few references to object.
var exercises = [Exercise]()
var equipments = [Equipment]()
func getAllExercises() {
let data = RealmService.shared.realm.objects(Exercise.self)
exercises = data.compactMap({$0})
let equipment = exercises.compactMap({$0.equipment})
for eq in exercises.compactMap({$0.equipment}) {
let numberOfEquipment = eq.components(separatedBy: ",")
for number in numberOfEquipment {
guard let intNumber = Int(number) else { return }
guard let finalEquipment = RealmService.shared.realm.object(ofType: Equipment.self, forPrimaryKey: intNumber) else { return }
equipments.append(finalEquipment)
}
}
Maybe the better option is to just insert values instead of object references?
You need to set up one-to-many relationships to take advantage of quicker queries and lazy loading.
I've simplified the models, but the magic is in the equipmentObjects property:
class Exercise: Object {
#Persisted(primaryKey: true) var exerciseID = 0
#Persisted var name: String = ""
#Persisted var equipment: String
#Persisted var equipmentObjects: List<Equipment>
convenience init(exerciseID: Int, name: String, equipment: String) {
self.init()
self.exerciseID = exerciseID
self.name = name
self.equipment = equipment
}
}
class Equipment: Object {
#Persisted(primaryKey: true) var equipmentID = 0
#Persisted var equipment: String = ""
convenience init(equipmentID: Int, equipment: String) {
self.init()
self.equipmentID = equipmentID
self.equipment = equipment
}
}
You can go ahead and initialize realm with your csv file. But when the app begins you would want to go ahead and establish the relationships between Exercise, Equipment, and Muscles. You should only do this once.
Here I've created a small utility to link the realm objects. Notice how it uses UserDefaults to check and see if relationships were already built. It is also building the relationships on a specified queue. You would want to pass in a background queue rather than the main queue so the UI doesn't lock up.
struct RealmRelationshipBuilder {
let configuration: Realm.Configuration
let userDefaults: UserDefaults
let queue: DispatchQueue
func buildRelationshipsIfNeeded(completion: #escaping() -> Void) {
guard userDefaults.didBuildRealmRelationships == false else { return completion() }
queue.async {
autoreleasepool {
defer { completion() }
do {
let realm = try Realm(configuration: configuration)
try realm.write {
realm.objects(Exercise.self).forEach { exercise in
let equipment = exercise
.equipment
.components(separatedBy: ",")
.compactMap(Int.init)
.compactMap { realm.object(ofType: Equipment.self, forPrimaryKey: $0) }
exercise.equipmentObjects.append(objectsIn: equipment)
}
}
} catch {
print("RealmRelationshipBuilder error: \(error)")
}
userDefaults.didBuildRealmRelationships = true
}
}
}
}
extension UserDefaults {
enum Key {
static let didBuildRealmRelationships = "didBuildRealmRelationshipsKey"
}
var didBuildRealmRelationships: Bool {
get { bool(forKey: Key.didBuildRealmRelationships) }
set { set(newValue, forKey: Key.didBuildRealmRelationships) }
}
}
Then to test the builder here is a small test case. But in reality you would probably want to show the user an status indicator while the relationships are being built in the background.
enum InitialData {
static let exercises: [Exercise] = {
[
Exercise(exerciseID: 1, name: "Bench press", equipment: "1,3,5"),
Exercise(exerciseID: 2, name: "Butterfly", equipment: "6"),
]
}()
static let equipment: [Equipment] = {
[
Equipment(equipmentID: 1, equipment: "Barbell"),
Equipment(equipmentID: 2, equipment: "Bench"),
Equipment(equipmentID: 3, equipment: "Bodyweight"),
Equipment(equipmentID: 4, equipment: "Cable"),
Equipment(equipmentID: 5, equipment: "Not sure"),
Equipment(equipmentID: 6, equipment: "Unknown"),
]
}()
}
class RealmExerciseTests: XCTestCase {
let realmConfiguration = Realm.Configuration.defaultConfiguration
override func setUpWithError() throws {
let realm = try Realm(configuration: realmConfiguration)
try realm.write {
realm.deleteAll()
realm.add(InitialData.exercises)
realm.add(InitialData.equipment)
}
}
func testInitialize() throws {
let relationshipBuilder = RealmRelationshipBuilder(
configuration: realmConfiguration,
userDefaults: .init(suiteName: UUID().uuidString) ?? .standard,
queue: DispatchQueue(label: "realm.init.background")
)
let expectation = expectation(description: "realm.init")
relationshipBuilder.buildRelationshipsIfNeeded {
expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
let realm = try Realm(configuration: realmConfiguration)
realm.refresh()
guard let exercise1 = realm.object(ofType: Exercise.self, forPrimaryKey: 1) else {
return XCTFail("Missing exercise with primary key 1")
}
guard let exercise2 = realm.object(ofType: Exercise.self, forPrimaryKey: 2) else {
return XCTFail("Missing exercise with primary key 2")
}
XCTAssertEqual(exercise1.equipmentObjects.count, 3)
XCTAssertEqual(exercise2.equipmentObjects.count, 1)
}
}
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
}
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!
I'm trying to get list of toys from Firestore and put it into array
But when I call function, it returns empty array, and just after returning it prints Toy object, so order is broken.
I thought that closures would help me, but I think I don't know how to use them, and examples from Google don't help me
Here is my code (I use SwiftUI so I created swift file with variable)
let db = Firestore.firestore()
class DataLoade {
func loadFirebase(completionHandler: #escaping (_ toys: [Toy]) -> ()){
var toysar: [Toy] = []
let toysRef = db.collection("Toys")
toysRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var name: String = document.get("name") as! String
var id: Int = document.get("id") as! Int
var description: String = document.get("description") as! String
var imageName: String = document.get("imageName") as! String
var price: String = document.get("price") as! String
var category: String = document.get("category") as! String
var timeToy = Toy(id: id, name: name, imageName: imageName, category: category, description: description, price: price)
toysar.append(timeToy)
}
}
}
completionHandler(toysar)
// print(toysar)
}
}
that's what it prints out:
[] // it prints empty array, but it is in the end of the code
Toy(id: 1001, name: "Pikachu", imageName: "pikachu-plush", category: "lol", description: "kek", price: "350₽") // and now it prints Toy object, however it is in the start of the code
Ok, so I tried to make completion handler for my function, like in "duplicated" answer, but that doesn't work: array is returning before completion handler works
ContentView.swift
func updateArray() -> [Toy]{
dl.loadFirebase() { toys in
ll = toys
}
print("lol \(datas)") // prints «lol []»
return ll
}
You can wait for an asynchronous task using a DispatchGroup. But the trick is NOT to associate asynchronous tasks with return statements. Instead, use closures to do an action after the task is done.
Disclaimer: I wrote this on SO, I apologize in advance for syntax issues.
let toyData = loadFirebase( { (toys) in
print(toys)
//Do something with toys when done
//You could add another completionHandler incase it fails.
//So 1 for pass and 1 for fail and maybe another for cancel. W/e u want
} )
let db = Firestore.firestore()
func loadFirebase(completionHandler:#escaping ((toys: [Toy]?) -> Void)) {
//Create Group
let downloadGroup = DispatchGroup()
var toysar: [Toy] = []
let toysRef = db.collection("Toys")
//If you had multiple items and wanted to wait for each, just do an enter on each.
downloadGroup.enter()
toysRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var name: String = document.get("name") as! String
var id: Int = document.get("id") as! Int
var description: String = document.get("description") as! String
var imageName: String = document.get("imageName") as! String
var price: String = document.get("price") as! String
var category: String = document.get("category") as! String
var timeToy = Toy(id: id, name: name, imageName: imageName, category: category, description: description, price: price)
toysar.append(timeToy)
print(timeToy)
}
//We aren't done until AFTER the for loop, i.e., each item is grabbed.
downloadGroup.leave()
}
}
//Once the queue is empty, we notify the queue we are done
downloadGroup.notify(queue: DispatchQueue.main) {
completionHandler(toys)
}
}
import SwiftUI
var dl = DataLoade()
var ll: [Toy] = []
let semaphore = DispatchSemaphore(value: 1)
struct ContentView: View {
var items: [Toy]
var body: some View {
NavigationView{
ScrollView(){
VStack(alignment: .leading){
ToyRow(category: "Наш выбор", toys: items)
Spacer()
ToyRow(category: "Акции", toys: items)
}
}.navigationBarTitle(Text("Игрушки г.Остров"))}
}
}
func upe(completionHandler:#escaping ((toys: [Toy]?){
dl.loadFirebase(completionHandler: { toy in
ll.append(contentsOf: toy!)
completionHandler(ll)
} )
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
upe(completionHandler: { (toys) in
DispatchQueue.main.async {
ContentView(items: toys)
}
})
}
}
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
}
}