Accessing Firestore data outside of Function [duplicate] - swift

This question already has an answer here:
Assign value of a Firestore document to a variable
(1 answer)
Closed 3 years ago.
I have a FireStore function in my FirestoreService file as below;
func retrieveDiscounts() -> [Discount] {
var discounts = [Discount]()
reference(to: .discounts).getDocuments { (snapshots, error) in
if error != nil {
print(error as Any)
return
} else {
guard let snapshot = snapshots else { return }
discounts = snapshot.documents.compactMap({Discount(dictionary: $0.data())})
}
}
return discounts
}
how do I get returned values to populate my private var discounts = [Discount]() variable in my viewController
Many thanks as always...

Your functions will get your UI to freeze until its operation is complete. The function which may take long duration to complete should be done asyncronous using escaping closures. The function should be like below :
func retrieveDiscounts(success: #escaping([Discount]) -> ()) {
var discounts = [Discount]()
reference(to: .discounts).getDocuments { (snapshots, error) in
if error != nil {
print(error as Any)
success([])
return
} else {
guard let snapshot = snapshots else { return }
discounts = snapshot.documents.compactMap({Discount(dictionary: $0.data())})
success(discounts)
}
}
}
Note: The data returns empty if error. Please handle error case if you need.
We first need an instance of FirestoreService class. Then the instance should call the retrieveDiscounts() function and populate it to our instance i.e. discounts.
Code:
class ViewController: UIViewController {
private var discounts = [Discount]() {
didSet {
self.tableView.reloadData()
}
}
func viewDidLoad() {
super.viewDidLoad()
FirestoreService().retrieveDiscounts { discounts in
self.discounts = discounts
}
}
}

Related

Swift - How to sort documents fetched from Firestore Database by date?

I have a tableViewController that gets loaded with documents fetched from Firestore Database. I would like to get them sorted by date and not ID, but I don't know where exactly to use sortby(). The documents already have a variable with the date, I am just not sure where I add the .sort(by: "date").
I already checked around, but most people have a very different code to populate their tableviews, and mine looks completely different.
Since I am new with Swift it took a lot of effort to get the tableViewController to work properly (mainly through online tutorials), but I don't fully understand it. This is the code:
This is an extension
class EncontradoService {
let database = Firestore.firestore()
func get(collectionID: String, handler: #escaping ([Encontrado]) -> Void) {
database.collection("EncontradosFinal")
.addSnapshotListener { querySnapshot, err in
if let error = err {
print(error)
handler([])
} else {
handler(Encontrado.build(from: querySnapshot?.documents ?? []))
}
}
}
}
And this is in the tableViewController
private var service: EncontradoService?
private var allencontrados = [Encontrado]() {
didSet {
DispatchQueue.main.async {
self.encontrados = self.allencontrados
}
}
}
var encontrados = [Encontrado]() {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func viewDidLoad()
{
super.viewDidLoad()
loadData()
}
func loadData() {
service = EncontradoService()
service?.get(collectionID: "EncontradosFinal") { encontrados in
self.allencontrados = encontrados
}
}
Thanks!

Swift AsyncStream variable, use after finish()

I'm currently reading about AsyncStreams and as I understand it, they are best for tasks that produce some results over time, but have a lifespan - e.g. start and end. Here's an example I am playing with:
struct AHardWorkingStruct {
lazy var updates = AsyncStream<String> { continuation in
onProgress = { value in
continuation.yield(value)
}
onFinish = { value in
continuation.yield(value)
continuation.finish()
}
}
private var onProgress: (String) -> Void = { _ in () }
private var onFinish: (String) -> Void = { _ in () }
func doSomeWork() async {
let numbers = (0..<20).map { anInt in
anInt^2
}
for number in numbers {
onProgress("The number is \(number)")
if(number == 20) {
onFinish("I'm DONE!")
}
}
}
}
I then have AHardWorkingStruct as a property in my view controller and use it like so:
class MyViewController: UIViewController {
var myHardWorker = AHardWorkingStruct()
#IBAction func tapToRun(_ sender: UIButton) {
Task {
await myHardWorker.doSomeWork()
}
}
override func viewDidLoad() {
super.viewDidLoad()
Task {
for await stream in myHardWorker.updates {
print(stream)
}
}
}
This works perfectly when I tap the button.
However, once I call finish() on the stream, I no longer get the updates from it - which I understand is expected behaviour.
My question is, how can I keep getting updates from a stream that is a variable on a long-lived object? Is there a "reset"? (other than getting rid of the lazy and nilling the variable)
I also have a 2nd part to this - what would be the best way to unit test an asyncstream? I've tried using expectations, but not sure if this is right.

Removing Firestore SnapshotListener

I am currently working on a valet app. I have two table views one for the Location of the worker, and then another that when you click on a location the cars that are under that location come up in the new tableview.
My current problem is that when I click on a new location the old snapshot listener is still on and if an update happens under that location the second table view changes back to the original location.
I tried to do what they say by removing listener but then it just doesn't work at all.
This is my code below
// func to select the individual row
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch tableView {
case SelectLocationTableView:
do {
if (allowed == false){ // postin data in table already but not reloading the page yet
Authenticate(title: "hi", message: "hi")
Location = postData[indexPath.row]
oldLocation=Location
}
}
do {
if (allowed == true) {
allowed = false
Location = postData[indexPath.row]
CarsRequestedTableView.reloadData()
DisplayLocations()
}
}
case CarsRequestedTableView:
CarSelected = postCars[indexPath.row]
WorkWithCar()
default:
print(Error.self)
}
}
// function to pull the location based on the cell you click on
func DisplayLocations (){
let listener = db.collection("Requested").document(Location).collection("Requested").addSnapshotListener { querySnapshot, error in
guard (querySnapshot?.documents) != nil else {
print("Error fetching documents: \(error!)")
return
}
self.postCars.removeAll()
for document in querySnapshot!.documents {
let post = document.documentID
self.postCars.append(post)
self.CarsRequestedTableView.reloadData()
self.DisplayAlert(title: "NEW REQUEST!", message: "CHECK FOR NEW REQUEST!")
}
}
}
You need to keep the listener registration in a (private) instance variable in your table view controller, and call its remove() method when you're ready to stop listening. The Firebase iOS Quickstart repo has a couple of classes that show how to do this, here is an example I took from RestaurantsTableViewController:
class RestaurantsTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// ...
// the data that's displayed in the table view
private var restaurants: [Restaurant] = []
private var documents: [DocumentSnapshot] = []
// this method removes any existing listener and sets up a new query
fileprivate var query: Query? {
didSet {
if let listener = listener {
listener.remove()
observeQuery()
}
}
}
// this is where you keep the listener registration
private var listener: ListenerRegistration?
// set up the new snapshot listener and map documents to data
fileprivate func observeQuery() {
guard let query = query else { return }
stopObserving()
// Display data from Firestore, part one
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Restaurant in
let maybeModel: Restaurant?
do {
maybeModel = try document.data(as: Restaurant.self)
} catch {
fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data()): \(error)")
}
if let model = maybeModel {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Missing document of type \(Restaurant.self) at \(document.reference.path)")
}
}
self.restaurants = models
self.documents = snapshot.documents
if self.documents.count > 0 {
self.tableView.backgroundView = nil
} else {
self.tableView.backgroundView = self.backgroundView
}
self.tableView.reloadData()
}
}
// this is where you stop the listener
fileprivate func stopObserving() {
listener?.remove()
}
/// ...
}

How to access variable in Document.swift from another class?

In CoreData document I have an entity SpaceLocation which is set of different SpaceLocations. One of them is default / current one, necessary for editing document.
It's defined in main Document.swift as:
#objc var defaultSpaceLocation: SpaceLocation {
return SpaceLocation.defaultSpaceLocation(in: managedObjectContext!)
}
#objc var currentSpaceLocation: SpaceLocation {
get {
if _currentSpaceLocation == nil {
willChangeValue(for: \Document.currentSpaceLocation)
_currentSpaceLocation = defaultSpaceLocation
didChangeValue(for: \Document.currentSpaceLocation)
}
return _currentSpaceLocation!
}
set {
willChangeValue(for: \Document.currentSpaceLocation)
_currentSpaceLocation = newValue
didChangeValue(for: \Document.currentSpaceLocation)
}
}
and in SpaceLocation class as:
class func defaultSpaceLocation(in moc: NSManagedObjectContext) -> SpaceLocation {
let spaceLocationsRequest = NSFetchRequest<SpaceLocation>(entityName: "SpaceLocation")
var result:SpaceLocation? = nil
do {
let spaceLocations = try moc.fetch(spaceLocationsRequest)
result = spaceLocations.filter {$0.isBaseLocation}.first!
}
catch {
Swift.print("•••• ERROR ••••", #file, #function, "Couldn't get Default Location")
}
return result!
}
}
I access to currentSpaceLocation by:
(NSDocumentController.shared.currentDocument as?Document)?.currentSpaceLocation
but it works only if document is in foreground. When app goes background NSDocumentController.shared.currentDocument becomes nil so I cannot access document.currentSpaceLocation. Any idea how to solve this?

for loop cycle showing always the last array value swift 3 after HTTP request

I already used another post to solver part of the problem,so now I am able to wait the end of the request. I made a http request with Almofire and parse the resulting JSON file. the code inside the myGroup.notify i think is correctly executed ater the request is completed (if I print allCards.count returns the correct value). Why if I put the for loop inside it is always showing the last card?
Thanks for any help
import UIKit
mport SwiftyJSON
import Alamofire
class ViewController: UIViewController {
//variables Declaration
var allCard = [Card]()
let allCardsHTTP: String = "https://omgvamp-hearthstone-v1.p.mashape.com/cards?mashape-key="
override func viewDidLoad() {
super.viewDidLoad()
let myGroup = DispatchGroup()
//get HTTp request with Alamofire
myGroup.enter()
Alamofire.request(allCardsHTTP, method: .get).responseJSON {
response in
if response.result.isSuccess {
let jsonCards : JSON = JSON(response.result.value!)
print("success")
self.updateJSONCards(json: jsonCards)
}
else {
print("error")
}
myGroup.leave()
}
myGroup.notify(queue: .main) {
//Updte UI here
print(self.allCard[10].name) //This is Working
for card in self.allCard {
if card.cardsSet == "Basic" {
print(card.name)
}
} //For loop always showing last card ???
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: - JSON Parsing
//Write the updateJSONCards method here:
func updateJSONCards(json: JSON) {
//create the cards
for (set, value) in json {
var card = Card()
card.cardsSet = set
if json[set].count != 0 {
for i in 0...json[set].count - 1 {
card.name = json[set][i]["name"].stringValue
allCard.append(card)
}
}
else {
print("The set \(set) has no cards")
}
}
}
}
You are creating the Card instance at the wrong place.
Before the inner repeat loop you are creating one instance and appending the same instance to the allCard array. If Card is a class (reference semantics) all occurrences of that instance contain the same data (the last loop item).
Put the two lines in the repeat loop.
//Write the updateJSONCards method here:
func updateJSONCards(json: JSON) {
//create the cards
for (set, value) in json {
if !json[set].isEmpty {
for i in 0..<json[set].count {
let card = Card()
card.cardsSet = set
card.name = json[set][i]["name"].stringValue
allCard.append(card)
}
}
else {
print("The set \(set) has no cards")
}
}
}