This question already has an answer here:
How to use downloaded URL correctly in AsyncImage?
(1 answer)
Closed 2 months ago.
My function ends before all data can be retrieved from the fire storage and returns empty.
How can I get the data before returning?
Code:
func listItem() -> [imageitem]{
let storage = Storage.storage()
let storageRef = storage.reference().child("images/image")
var array = [imageitem]()
storageRef.listAll { (result, error) in
if let error = error {
print("error")
print(error)
}
print("storagereference")
result?.items.forEach({ StorageReference in
print(StorageReference)
print(StorageReference.name)
StorageReference.getData(maxSize: 5*1024*1024) {data, error in
if error == nil && data != nil{
if let image = UIImage(data: data!){
let element = imageitem(title: StorageReference.name, image: Image(uiImage: image))
array.append(element)
print(array)
}
}
}
})
print("end",array)
}
return array
}
Console:
storagereference
gs://proj-69776.appspot.com/images/image/gorilla.jpg
gorilla.jpg
gs://proj-69776.appspot.com/images/image/test.jpg
test.jpg
end []
[proj.ListView.imageitem(title: "test.jpg", image: SwiftUI.Image(provider: SwiftUI.ImageProviderBox<__C.UIImage>))]
[proj.ListView.imageitem(title: "test.jpg", image: SwiftUI.Image(provider: SwiftUI.ImageProviderBox<__C.UIImage>)), proj.ListView.imageitem(title: "gorilla.jpg", image: SwiftUI.Image(provider: SwiftUI.ImageProviderBox<__C.UIImage>))]
adding an await before the addData gives me an error
Cannot pass function of type '(StorageReference) async -> Void' to parameter expecting synchronous function type
You cannot return data that is loaded asynchronously with a return statement like that, because the return runs well before you ever append any element to the array. If you set breakpoints and run the code in a debugger you can most easily validate that.
Instead, you'll want to pass in a closure/callback or use a dispatch queue to get the value out of your function into the calling context.
For examples of how to do this, see:
Return image from asynchronous call
Unable to assign value within a function in Firebase API callback block
When I try to access my array outside of this function it appears to be empty.I t is something to do with an asynchronous call, any suggestion?
Accessing Array Outside Closure in Swift 3
How to make synchronous operation with asynchronous callback?
How to upload the profile image URL to firebase database
Related
I have tried to return an Image or UIImage from the function below but there is an issue that says
"Unexpected non-void return value in void function."
I am retrieving the Image from Firebase Storage and just want to return it...
func retrivePhotos(path: String) -> UIImage {
let storageRef = Storage.storage().reference()
let fileRef = storageRef.child(path)
// Retrieve the data with the path
fileRef.getData(maxSize: 5 * 1024 * 1024) { data, error in
if error == nil && data != nil {
let image = UIImage(data: data!)
return image
}
}
}
The thing to understand is that your call to getData(maxSize:) is a separate asynchronous function. That function does not, and cannot, return the image in its return. It needs to start a network request. It starts that network request and then returns immediately. When the function returns, the network request hasn't even begun sending over the network yet.
The return in the braces following that function call are a return from a closure, which is a block of code that you are passing to the function.
Your function also cannot return an image if it is getting the image using an async function. (unless you rewrite it using async/await, a new language feature that is a little beyond the scope of this question, and likely beyond your current understanding.)
You need to rewrite your function to take a completion handler, just like the getData(maxSize:) function takes a completion handler:
typealias ImageHandler = (UIImage?) -> Void
func retrivePhotos(path: String, completion: ImageHandler) {
let storageRef = Storage.storage().reference()
let fileRef = storageRef.child(path)
// Retrieve the data with the path
fileRef.getData(maxSize: 5 * 1024 * 1024) { data, error in
if error == nil && data != nil {
let image = UIImage(data: data!)
completion(image)
} else {
completion(nil)
}
}
}
(That is over-simplified. You'd probably write it to take a Result instead of an Optional UIImage.)
Think of async functions like this. The main program is you, cooking dinner for your family. You realize you are out of milk, so you send your kid to the store to buy some. You don't hand your kid some money and expect them to hand you back the milk right then and there. You hand them the money, tell them to get you milk, and then go back to the rest of the cooking. At some time in the future, your kid comes back and either gives you the milk (The success case) or tells you there was a problem that prevented them from getting the milk (the error case.)
At the point your kid come back from the store, you run the "kid is back from the store" completion handler. You either go into sauce-making mode with the milk, or an error handler where you decide what to do since you don't have milk.
This question already has answers here:
Return image from asynchronous call
(1 answer)
Unable to assign value within a function in Firebase API callback block
(1 answer)
Cannot get value of downloadURL fire storage swift
(1 answer)
Closed last year.
I have a working function that downloads data from my Firebase Storage, but I don't understand how to actually use the data in my project.
func getImg2() {
let storageRef = storage.reference()
let imageRef = storageRef.child("images/hi2.png")
_ = imageRef.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print(error.localizedDescription)
} else {
let image = UIImage(data: data!)
print("Data: \(data!)")
print("succesfully downloaded \(image!)")
}
}
}
The data gets printed, as well as the image information. I would like to return this data and use it outside the function to load a UIImage. Everything I've tried has given me an error or just returned "0 bytes" for the data. I've been stuck on this for days and would greatly appreciate any help.
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed last year.
I'm new at Swift and that's why i need your help. So I have a function which should send request and return a value
func getAnswer() -> String? {
var answer: String?
guard let url = URL(string: "https://8ball.delegator.com/magic/JSON/_") else { return nil }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
return
}
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode == 200 else { return }
do {
let model = try JSONDecoder().decode(Answer.self, from: data)
DispatchQueue.main.async {
answer = model.magic.answer
}
} catch let error {
fatalError(error.localizedDescription)
}
}.resume()
return answer
}
but it always returns nil.
I suppose problem is here
DispatchQueue.main.async {
answer = model.magic.answer
}
How can I fix it?
In order to know what is happening here, you need to learn about #escaping functions in swift, here is some link1 together with taking function as another functions parameter link2 written in part "Function Types as Parameter Types" , closures in Swift link3 and
Here is what is happening simplified and explained step by step :
you call getAnswer()
variable answer gets initialized with value nil by declaring answer: String?
URLSession.shared.dataTask is called and it is taking as an argument another function - closure (Data?, URLResponse?, Error?) -> Void . Also URLSession.shared.dataTask is executed on different thread and is not returning yet, but will return right after it receives response from server, which can take any time (but usually milliseconds) and will basically happen after your getAnswer() function is returning value.
your getAnswer() immediately returns value of answer which is currently nil
if you get any data from server, or server could not be reached, your URLSession.shared.dataTask function executes your code in closure. This is the code it will execute:
guard let data = data, error == nil else {
return
}
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode == 200 else { return }
do {
let model = try JSONDecoder().decode(Answer.self, from: data)
DispatchQueue.main.async {
answer = model.magic.answer
}
} catch let error {
fatalError(error.localizedDescription)
}
Your problem lies in how swift executes closures. When you call
URLSession.shared.dataTask(with: url) {
// Closure code here
}
return answer
Your "Closure code here" doesn't get called until the endpoint "https://8ball.delegator.com/magic/JSON/_" actually gives a response. However, you've promised swift that your function will return an optional string immediately after the serial code of your function has completed. For this reason, by the time your "Closure code here" has run, and your "answer" variable has been updated with the correct value, your function is long gone, and has already returned a value (which in this case is whatever you've set it to at the beginning - nil).
You can fix this issue in one of two ways.
Swift's new concurrency system
By defining your own closure.
Swift's new concurrency system
You can define your function as async, meaning that the function won't have to return a value in serial, as follows.
enum GetAnswerError: Error {
case invalidURL
}
func getAnswer() async throws -> String {
var answer: String?
guard let url = URL(string: "https://8ball.delegator.com/magic/JSON/_") else {
throw GetAnswerError.invalidURL
}
// Your function will suspend here and probably be moved to a different thread. It will resume once a response has been received from the endpoint.
let (data, _) = try await URLSession.shared.dataTask(with: url)
let parsedData = try JSONDecoder().decode(Answer.self, from: data)
return parsedData.magic.answer
}
When you call this function, you'll have to do so from an environment which swift can suspend. This means you'll call the function from either another async function like so
func anotherFunction() async throws -> Bool {
let answer = try await getAnswer()
// Run some code here
return answer == "YES" // Return some useful value
}
or from a Task object like so
Task {
// Note that because the function getAnswer() can throw errors, you'll have to handle them when you call the function. In this case, I'm handling them by using try?, which will simply set answer to nil if an error is thrown.
let answer = try? await getAnswer()
}
Note that when you call code in a task, you must be using the return value's from within the scope of the task. If you try to do something like this
func getAnswerTheSecond() -> String? {
var answer: String? = nil
Task {
let receivedAnswer = try? await getAnswer()
answer = receivedAnswer
}
return answer
}
You'll just end up back where you started, where swift immediately returns the nil value because your code is ran in serial. To fix this, run the relevant code on the "answer" from wherever it is needed within the task. If you are using the "answer" to update a SwiftUI view that might look like this.
struct ContentView: View {
#State var answer: String = ""
// This is the function that I've written earlier
func getAnswer() async throws -> String {
// Make URL Request
// Return the value
}
var body: some View {
Text(self.answer)
.onAppear{
Task{
let result = try? await self.getAnswer()
self.answer = result
}
}
}
}
Defining your own closure
You can define your own closure to handle the URL response; however, because of swift's new concurrency framework, this is probably not the right way to go.
If you'd like to go this way, do a google search for "Swift closures", and you'll find what you need.
The following is pseudo-code to help demonstrate my question:
var dataSourceArray = [CustomClassObject]()
databaseListener { (data) in
dataSourceArray.removeAll()
let id = data.id
let imageURL = data.imageURL
let listing = CustomClassObject(id: id, image: nil)
remoteFileStorage.getImage(url: imageURL) { (image) in
listing.image = image
}
dataSourceArray.append(listing)
tableView.reloadData()
}
databaseListener is a realtime database listener that can return updates rapidly. In its completion handler is an async call that downloads an image. If databaseListener returns new data before remoteFileStorage has a chance to return from the last run, what happens to the listing pointer in the remoteFileStorage closure? The listing pointer no longer has a home in dataSourceArray (it was just purged) so can the remoteFileStorage handler safely access it anyway? My assumption is that the listing pointer is still pointing to a valid address in memory since the remoteFileStorage closure has a reference to it, but I am not certain?
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I'm trying to get current document count belongs to spesific user from Firebase.
Whenever i try to assign count value to my variable in closure, value is always shown as nill.
So that i did couple of research, as a result i figured out networking sometimes takes so long and it happens asynchronously. So if i'm not wrong due to asynchronous returning a value inside a fucntion might happen before assign value .
I tried to add dispatchqueue.main.async but it didn't work for me...
Here is my code
func getEventCount () -> Int? {
var count: Int?
db.collection("Events").whereField("owner", isEqualTo: currentUser.email).getDocuments { (snapshot, error) in
if error != nil {
print(error)
}else {
DispatchQueue.main.async {
if let snapshot = snapshot {
count = snapshot.count
}
}
}
}
return count
}
My main goal is getting count data from database and assign it to 'count' variable.
So why do i need that count variable? Because i'm going to pass that count value to tableView datasource method numberOfRowsInSection which is expecting a int value. With that value i'm going to represent some data in Events document from firestore in table views.
note: When i try to print count value in closure it shows desired value, but when function return value it's shown nill...
Once it is a Async call - you cannot synchronously return the value from the function. You should accept a callback to the function that will accept the count. That callback function or closure will be passed the value asynchronously.
func getEventCount (callback: #escaping(Result<Int, Error>) -> Void) {
db.collection("Events").whereField("owner", isEqualTo: currentUser.email).getDocuments { (snapshot, error) in
if error != nil {
let result = Result.failure(error)
callback(result)
}else if let snapshot = snapshot {
let result = Result.success(snapshot.count)
callback(result)
} else {
let result = Result.failure(SomeCustomAppError)
callback(result)
}
}
}
Then you can call this function passing in a callback
self.getCount() { result in
switch result {
case .success(let count): /// use count
print(count)
// only here u can assign the count value to ur variable
case .error(let error): /// handle error
print(error.localizedDescription)
}
}
Note: In the above I've used the Result datatype from Swift standard library - https://developer.apple.com/documentation/swift/result so that both error
or result can be passed back