Swift + Realm: How to change an outer variable from inside a collection.find's scope - swift

Following this tutorial, I've written the following class:
import RealmSwift
import Darwin
import SwiftUI
let app = App(id: "my-app-id")
class AccessManager: Object {
#objc dynamic var isInTime: Bool = false
func foo2() -> Bool {
return true
}
func foo1() {
app.login(credentials: Credentials.anonymous) { (result) in
DispatchQueue.main.async {
switch result {
case .failure(let error):
print("Login failed: \(error)")
case .success(let user):
print("Login as \(user) succeeded!")
let client = app.currentUser!.mongoClient("mongodb-atlas")
let database = client.database(named: "my-database")
let collection = database.collection(withName: "my-collection")
let identity = "my-identity"
collection.find(filter: ["_partition": AnyBSON(identity)], { (result) in
switch result {
case .failure(let error):
print("Call to MongoDB failed: \(error.localizedDescription)")
case .success(let documents):
self.bar = self.foo2()
print(self.bar) // prints true
}
})
print(self.bar) // prints false
}
}
}
}
}
When I change the value of self.bar inside of collection.find's scope (using the self.foo2 function), its value doesn't change outside of that scope - i.e in the first print(self.bar) - true is being printed, but in the second one - false is printed.
How can I change self.bar's value so the change will also take effect outside of collection.find's scope?

As #Jay commented:
closures are asynchronous and the code following the closure will
(may) execute before the code in the closure. So that code will print
false before the value is set to true. Code is faster than the
internet so data is only valid in the closure.
This is why in my case, the print(self.bar) outside of the closure was executed before the collection.find closure. Therefore it's result was false instead of true.

Related

How to access associated value of a switch case, which itself is in a trailing closure

As follows, what expression should I write to access let learningList? In the code, query is a class, find is a function. Many thanks!
_ = query.find { result in
switch result {
case .success(objects: let learningList):
break
case .failure(error: let error):
print(error)
}
}
That is a local variable, so you can access it only in the decelerated context, i.e. inside case. If you need to use it later you have to create a property in caller class to hold it, like
_ = query.find { [weak self] result in // << ref to caller
switch result {
case .success(objects: let learningList):
self?.learningList = learningList // << safe in caller's property
break
case .failure(error: let error):
print(error)
}
}

Variable 'theData' used before being initialized, How should I fix

I am trying to Apollo framework and a graphql api to obtain the data then return it. Once I have the data in another swift file, I want to call on certain parts of the data and assign it to a variable. The errors I get is variable used before it is initialized. and if try to return the variable from within the closure I get "Unexpected Non-Void Return Value In Void Function ". I heard of ways to get around that error but I don't completely understand it and how it works with my code. If you need more code or context you can message me and I can share my GitHub repo. Sorry if the code is bad, please don't roast me. I am still a beginner.
import Foundation
import Apollo
struct AniListAPI {
let aniListUrl = "https://graphql.anilist.co"
func ObtainData(AnimeID: Int)-> QueryQuery.Data{
var theData: QueryQuery.Data
let theInfo = QueryQuery(id: AnimeID)
GraphClient.fetch(query: theInfo) { result in
switch result {
case .failure(let error):
print("A big No no happened \(error)")
case .success(let GraphQLResult):
guard let Info = GraphQLResult.data else {return}
theData = Info
}
}
return theData
}
}
Unexpected Non-Void Return Value In Void Function.
The reason you're getting this warning is because you can't return value from inside the closure. Use closure instead of returning value.
func ObtainData(AnimeID: Int, completion: #escaping (Data) -> Void) {
var TheData: QueryQuery.Data
let TheInfo = QueryQuery(id: AnimeID)
GraphClient.fetch(query: TheInfo) { result in
switch result {
case .failure(let error):
print("A big no no happened retard \(error)")
case .success(let GraphQLResult):
guard let Info = GraphQLResult.data else {return}
TheData = Info
completion(TheData)
}
}
}
and call it like..
ObtainData(AnimeID: 123) { (anyData) in
print (anyData)
// continue your logic
}

How to handle priorities in a Swifty JSON Alamofire request?

How can I use the dispatchQueue or something like "await" in Javascript to return a value in self.arrayData (because the end of my loop is ran before the previous content). I am used to R and Python where the code runs line by line, What is the best behavior to adopt in Swift ?
Here is the function :
func fetch2(){
var i:Int = 0
repeat {
AF.request(itemLookUp[i]).validate().responseJSON { response in
switch response.result {
case .failure(let error):
print("\(error) in fetch2")
case .success(let value):
let json = JSON(value)
//Extract the Matiere for ML Extraction
self.matiereInput = json["ResultSet"]["0"]["Result"]["0"]["SpAdditional"].string ?? "none"
let energyCheck:Bool = self.matiereInput.contains("エネルギー") //energy-kcal
if energyCheck==true && self.arrayData[0]==0.0{
//regular expression
var patEnergy = #"(エネルギー)(([^\d]+)(\d+)(\.)(\d+)|([^\d]+)(\d+))"# //avoid the repetition of the pattern within the same matiereinput
let patEnergy2 = self.matches(for: patEnergy, in: self.matiereInput)
patEnergy = patEnergy2.joined(separator:"")
let valueEnergy = self.matches(for: self.regex2, in: patEnergy)
self.arrayData[0] = Double(valueEnergy.joined(separator: "")) ?? 0.0
}
}
}
i = i+1
print(self.arrayData[0])
} while i <= (self.returned-1)
}
Thank you in advance !
The standard pattern is notify with a DispatchGroup, and then use a completion handler to asynchronously notify the caller of the result:
func fetchAll(completion: #escaping (Result<[Double], Error>) -> Void) {
let group = DispatchGroup()
var results: [Double] = []
var errors: [Error] = []
for item in lookupItems {
group.enter() // enter before request
AF.request(item).validate().responseJSON { response in
defer { group.leave() } // leave when this closure is done
switch response.result {
case .failure(let error):
errors.append(error)
case .success(let value):
let result = ...
results.append(result)
}
}
}
group.notify(queue: .main) {
if let error = errors.first { // I don’t know what you want to do if there were multiple errors, so for now I’ll just grab the first one
completion(.failure(error))
} else {
completion(.success(results))
}
}
}
And then you’d use it like so:
fetchAll { result in
switch result {
case .failure(let error):
print(error)
case .success(let values):
print(values)
}
}
Now, I wasn’t able to reverse engineer what you were trying to do (you appear to be updating self.arrayData[0] in every iteration!), so I just returned an array of Double. But you can obviously change the type of results and the parameter of the completion closure to match whatever is relevant in your case.
But don’t get lost in the details of the above example, but rather just focus on a few key observations:
Supply completion handler closure to call when all the requests are done.
Use DispatchGroup to keep track of when all the requests are done.
Supply a notify closure to your DispatchGroup which will be called when all the group.enter() calls are offset by their respective group.leave() calls.
A more subtle observation is that you should refrain from updating properties from within the responseJSON block. Within your asynchronous code, you really want to limit your interaction to local variables if at all possible. Pass the result back in the completion closure (and the caller can update the model and the UI as it sees fit).

Value just available after second function call

I have the following function and it's working great but now I need the value out of the function to use it for another purpose.
But my problem is that I always have to execute the function twice to get a value in the outer part of the function (var valueOutOfFunction).
var valueOutOfFunction = [(String)]()
func loadQuery(com:#escaping( [(Int, String)] -> ())){
var arrayOfTuples = [(Int, String)]()
db.collection("Data").whereField("age", isGreaterThanOrEqualTo: 1).whereField("age", isLessThanOrEqualTo: 50).whereField("gender", isEqualTo: "F").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for (index, document) in querySnapshot!.documents.enumerated() {
arrayOfTuples += [(index, document.documentID)]
}
}
com(arrayOfTuples)
}
}
Then I am calling it here:
loadQuery { arr in
self.valueOutOfFunction = arr // Has a value in the first execution
}
print (valueOutOfFunction) //Executing the first time there is no value in the variable, the second time it have a value.
Why is it just on the second attempt available and what could be a solution for this problem ?
Thanks!
That is because the valueOutOfFunction is happening inside of that closure which is being called asynchronously to the print statement. Additionally, you are executing the completion outside of the else statement which may be firing before your for loop completes. What you want to do is control the flow from within the closure itself like so:
// move com(arrayOfTuples) up into the else block but outside of the for loop
func handleQuery() {
loadQuery { doStuffWithResult($0) }
// This line will run before the first line of doStuffWithResult
// It would be best to end the function logic at loadQuery
}
func doStuffWithResult(_ arrayOfTuples: [(Int, String)]) {
print(arrayOfTuples)
// Do other work here
}
What you're looking into is control flow. You want to execute X if Y has occurred and Z if !Y right?
I would recommend looking into swift's result type. This will help you manage control flow with closures.
An example:
typealias QueryResult = [(Int, String)]
enum QueryErrors: Error {
case couldNotFind
}
func loadQuery(_ result: #escaping (Result<QueryResult, Error>) -> Void) {
db.collection("Data").whereField("age", isGreaterThanOrEqualTo: 1).whereField("age", isLessThanOrEqualTo: 50).whereField("gender", isEqualTo: "F").getDocuments() { (querySnapshot, err) in
guard let documents = querySnapshot?.documents.enumerated() else {
// I think enumerated will give you an array of tuples... if not add this to the end of that line. After enumerated but before else
// enumerated().compactMap { ($0, $1) }
result(.failure(err ?? QueryErrors.couldNotFind))// passes back Firebase error if it exists or default error if it doesn't
return
}
result(.success(documents))
}
}
// how it works
loadQuery() { result in
switch(result) {
case .success(let arr): print(arr)
case .failure(let error): print(error)
}
}

Set value of array via KeyPath

I am trying to update the value stored in an array property of a class via the use of KeyPaths. Here is my code:
func listenAndUpdateDocuments<T: JSONDecodable>(
_ property: ReferenceWritableKeyPath<MyModel, [T]?>,
from model: MyModel) {
guard let reference = reference else {
return
}
guard listener == nil else {
return
}
listener = backendClient.listenToDocuments(reference) { [weak model] (result: Result<[T], RequestError>) in
switch result {
case .success(let value):
model?[keyPath: property] = value
case .failure:
model?[keyPath: property] = []
}
}
}
The problem is when I call this function like this:
myListener.listenAndUpdateDocuments(\.viewers, from: self)
where viewers is of type [ViewersModel], it always comes back with the following error:
Type of expression is ambiguous without more context
How do I solve this? I have a similar version of the code but where the property parameter isn't an array, and that works.
I struggled with something similar:
_ = Token.query(on: req).filter(\.expiry < Date()).delete()
The solution I found was to use a more up-to-date api to handle my request parameters.
_ = Token.query(on: req).filter(\.expiry, .lessThan, Date()).delete()
It had less to do with the keypath itself than I thought!