Download Image for Firebase Storage inside a SwiftUI Listview (contact app) - swift

I'm new in Swift and I'm trying to display inside a SwiftUI ListView :
some datas
an image (linked to datas)
It's kind of a contact app.
All these datas are stored in firestore. I created a function which gives the image URL on firestore :
func getURL(path: String, completion: #escaping (((URL?) -> Void))) {
let storage = Storage.storage()
storage.reference().child(path).downloadURL(completion: { url, error in
guard let url = url, error == nil else {
return
}
let urlPath = url.absoluteURL
completion(urlPath)
})
}
But when i call this function in the SwiftUI View, the following error appears :
"Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols"
There is the calling code of the function :
getURL(path: bike.access1, completion: { path in AnimatedImage(url: path)})
I'm aware that i can't call function inside a view, but i don't see how to manage to display an image from Firestore inside a SwiftUI List View.
If anyone know a strategy, I'm really interested.
Thanks in advance
Jean

The completion handler is (in general) a great way of dealing with asynchronous code. But, it a View in SwiftUI, it's a little more common to use a #State or #Published value and then render the view conditionally based on its state.
I like using an ObservableObject view model for this sort of thing:
class ViewModel : ObservableObject {
#Published var imageURL : URL?
func getURL(path: String) {
let storage = Storage.storage()
storage.reference().child(path).downloadURL(completion: { url, error in
guard let url = url, error == nil else {
return
}
self.imageURL = url
})
}
}
struct ContentView : View {
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
if let url = viewModel.imageURL {
AnimatedImage(url: url)
}
}.onAppear {
viewModel.getURL(path: "URL_STRING_GOES_HERE")
}
}
}
Note that my types

Related

SwiftUI: Display file url from Array after user Picks file using DocumentPicker

I followed a tutorial on getting Url from a Document a user chooses and be able to display it on the View. My problem now is I want to add those Url's into an array. Then get the items from the array and print them onto the View. The way it works is the User presses a button and a sheet pops up with the files app. There the user is able to choose a document. After the user chooses the document the Url is printed on the View. To print the Url is use this
//if documentUrl has an Url show it on the view
If let url= documentUrl{
Text(url.absoluteString)
}
Issue with this is that when I do the same thing the
If let url= documentUrl
Is ran before the Url is even added to the array and the app crashes
Here is the full code
//Add the Urls to the array
class Article: ObservableObject{
var myArray:[String] = []
}
struct ContentView: View {
#State private var showDocumentPicker = false
#State private var documentUrl:URL?
#State var myString:URL?
#ObservedObject var userData:Article
// Func for onDismiss from the Sheet
func upload() {
// add the Url to the Array
DispatchQueue.main.async{
userData.myArray.append(documentUrl!.absoluteString)
}
}
var body: some View {
VStack{
//If have Url reflect that on the View
if let url = documentUrl{
//Works
Text(url.absoluteString)
//doesntwork
Text(userData.myArray[0])
}
}
Button(action:{showDocumentPicker.toggle()},
label: {
Text("Select your file")
})
.sheet(isPresented: $showDocumentPicker, onDismiss: upload )
{
DocumentPicker(url: $documentUrl)
}
}
}
The main thing I want to do the just display the ulrs into the view after the user chooses the document or after the sheet disappears. So if the user chooses 1 Url only one is printed. If another one is chosen after then 2 are show etc.
This is the documentPicker code used to choose a document
struct DocumentPicker : UIViewControllerRepresentable{
#Binding var url : URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
//initialize a UI Document Picker
let viewController = UIDocumentPickerViewController(forOpeningContentTypes: [.epub])
viewController.delegate = context.coordinator
print("1")
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
print("Swift just updated ")
print("2")
}
}
extension DocumentPicker{
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator:NSObject, UIDocumentPickerDelegate{
let parent: DocumentPicker
init(_ documentPicker: DocumentPicker){
self.parent = documentPicker
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else{return}
parent.url = url
print("3")
}
}
}
Not sure if maybe I'm not approaching this the correct way? I looked at different tutorial but couldn't find anything.
Use .fileImporter presentation modifire (above ios 14)
.fileImporter(isPresented: $showDocumentPicker,
allowedContentTypes: [.image],
allowsMultipleSelection: true)
{ result in
// processing results Result<[URL], Error>
}
An observable object doesn't have a change trigger. To inform that the observable object has changed use one of the following examples:
class Article: ObservableObject {
#Published var myArray:[String] = []
}
or
class Article: ObservableObject {
private(set) var myArray:[String] = [] {
willSet {
objectWillChange.send()
}
}
func addUrl(url: String) {
myArray.append(url)
}
}
official documentation: https://developer.apple.com/documentation/combine/observableobject

Wait for request to be precessed for displaying information [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 1 year ago.
I want to make a Swift (5.4.6) app to display information about cryptos. For this I need to call a server to get the information I want to display. However my application don't wait for the data to be processed so I get an error for wanting to display a nil.
Here is my code for the call to the server :
var cryptos = Cryptos(cryptos: nil)
let url = URL(string: urlString)
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) {(data, response, error) in
if error == nil && data != nil {
let decoder = JSONDecoder()
do {
cryptos = try decoder.decode(Cryptos.self, from: data!)
print(cryptos.cryptos![6].name)
}
catch {
print("Impossible de convertir les données reçues")
}
}
}
dataTask.resume()
return cryptos
}
Here are the struct I want to decode the json into :
struct Crypto: Codable {
let name: String
let symbol: String
let price: Float
let totalSupply: Float
let marketCap: Float
let change24h: Float
}
struct Cryptos: Codable {
let cryptos: [Crypto]?
}
Finally here is the code in my view :
var lesCryptos = Cryptos()
struct CryptoList: View {
var body: some View {
HStack {
NavigationView {
}
}
.onAppear(perform: getAllCryptosInfos)
}
}
func getAllCryptosInfos() {
lesCryptos = Worker().getAllCryptosInfos()
print(lesCryptos.cryptos![7].name)
}
The error appears when I want to print the name ("lesCryptos.cryptos!" is nil)
Also here is a sample of the JSON i get :
{
"cryptos": [
{
"name":"Bitcoin",
"symbol":"BTC",
"price":36301.41347043701,
"totalSupply":18728856,
"marketCap":679883945484.275,
"change24h":0.36443243
},
{
"name":"Ethereum",
"symbol":"ETH",
"price":2784.5450982190573,
"totalSupply":116189845.499,"marketCap":323535864747.07007,
"change24h":3.46116544
}
]
}
First, convert your struct to a class and setup default values so that there is no nil values to cause crashes.
class Crypto: Codable {
let name: String
let symbol: String
let price: Float
let totalSupply: Float
let marketCap: Float
let change24h: Float
init() {
name = ""
symbol = ""
price = 0.0
totalSupply = 0.0
marketCap = 0.0
change24h: 0.0
}
}
In my example, notice that the init() constructor takes in no values. This allows me to create an object Crypto() without passing any values. This means that my object will have those default values, which is useful in your case because you're crashing from nil values.
struct ExampleView: View {
#State var someCrypto = Crypto()
var body: some View {
Text("\(someCrypto.name)"
.onAppear(
someCrypto.name = "Example"
)
}
}
In this example I'm using my default constructor for my Crypto object. That ensures that my view can display the name "" given by someCrypto.name. Then I use onAppear to simulate fetching of your data from the API. You will notice that the view updates automatically. This is because of the #State object which essentially tells the view to listen for any changes, and those changes are two-way. While in this context a Text() is not actually going to use it two-way a TextField() would.
Additional reading and studying should be looked up for #State #ObservedObject #ObservableObject and #Published which will help you get off your feet and use SwiftUI much more responsively.
Thank you for all your responses, however I managed to resolve my problem using completionHandler. Here is the link to the question and answers that helped me : stackoverflow question

Unable to use a defined state variable in the init()

I am trying to implement a search bar in my app, as now I want to use the keyword typed in the search bar to make an API call to fetch backend data, here is my code:
struct SearchView: View {
#State private var searchText : String=""
#ObservedObject var results:getSearchList
init(){
results = SearchList(idStr: self.searchText)
}
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
I implement SearchBar view followed the this tutorial https://www.appcoda.com/swiftui-search-bar/ exactly,
and getSearchList is a class which has an var called idStr,
struct searchResEntry: Codable, Identifiable{
var id:Int
var comment:String
}
class SearchList: ObservableObject {
// 1.
#Published var todos = [searchResEntry]()
var idStr: String
init(idStr: String) {
self.idStr = idStr
let url = URL(string: "https://..." + idStr)!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
// 3.
let decodedData = try JSONDecoder().decode([searchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
the problem I am struggling now is that I want to use the variable searchText to initialize the getSearchList , getSearchList has an var called idStr, this idStr is to used to store the typed keyword, my code always get an error: 'self' used before all stored properties are initialized , I have no idea how to deal with this.
Here is your code, edited by me:
struct SearchView: View {
#StateObject var results = SearchList()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $results.searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
struct SearchResEntry: Codable, Identifiable {
var id:Int
var backdrop_path:String
}
class SearchList: ObservableObject {
#Published var todos = [SearchResEntry]()
#Published var searchText: String = ""
var cancellable: AnyCancellable?
init() {
cancellable = $searchText.debounce(
for: .seconds(0.2),
scheduler: RunLoop.main
).sink { _ in
self.performSearch()
}
}
func performSearch() {
if let pathParam = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
let url = URL(string: "https://hw9node-310902.uc.r.appspot.com/mutisearch/\(pathParam)") {
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
let decodedData = try JSONDecoder().decode([SearchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
} else {
print("Invalid URL")
}
}
}
Explanation
You are free to reverse the optional changes i made, but here are my explanations:
Use capital letter at the beginning of a Type's name. e.g write struct SearchResEntry, don't write struct searchResEntry. This is convention. Nothing big will happen if you don't follow conventions, but if anyone other than you (or maybe even you in 6 months) look at that code, chances are they go dizzy.
Dont start a Type's name with verbs like get! Again, this is just a convention. If anyone sees a getSomething() or even GetSomething() they'll think thats a function, not a Type.
Let the searchText be a published property in your model that performs the search. Don't perform search on init, instead use a function so you can initilize once and perform search any time you want (do results.performSearch() in your View). Also you can still turn your searchText into a binding to pass to your search bar (look at how i did it).
EDIT answer to your comment
I could right-away think of 3 different answers to your comment. This is the best of them, but also the most complicated one. Hopefully i chose the right option:
As you can see in the class SearchList i've added 2 things. First one is a cancellable to store an AnyCancellable, and second is the thing in init() { ... }. In init, we are doing something which results in an AnyCancellable and then we are storing that in the variable that i added.
What am i doing In init?
first $searchText gives us a Publisher. Basically, the publisher is called whenever the searchText value changes. Then you see .debounce(for: .seconds(0.2), on: RunLoop.main) which means only let the latest input go through and reach the next thing (the next thing is .sink { } as you can see), only if the user has stopped writing for 0.2 seconds. This is very helpful to avoid a load of requests to the server which can eventually make servers give you a 429 Too Many Requests error if many people are using your app (You can remove the whole .debounce thing if you don't like it). And the last thing is .sink { } which when any value reaches that point, it'll call the performSearch func for you and new results will be acquired from the server.
Alternative way
(again talking about your comment)
This is the simpler way. Do as follows:
remove init() { ... } completely if you've added it
remove var cancellable completely if you've added it
in your SearchView, do:
.onChange(of: results.searchText) { _ in
results.performSearch()
}
pretty self-explanatory; it'll perform the search anytime the searchText value is changed.

Reload Image if value in EnvironmentObject changes

I am showing a Card incl. a recipe summary in my app. Via an Edit-Sheet, I am able to change the respecting recipe to a new one. After saving, all the information in the card are updated, but the picture of the old recipe is still shown. Only after I go back and forth in the navigation tree, the picture is updated.
Is there an easy way to tell my ImageLoaderView to reload the picture whenever the value of the recipeVM changes?
import FirebaseStorage
import SDWebImageSwiftUI
import SwiftUI
struct ImageLoaderView: View {
#EnvironmentObject var recipeVM: RecipeViewModel
let storage = Storage.storage()
#State private var imageURL = URL(string: "")
var body: some View {
WebImage(url: imageURL)
.resizable()
.placeholder {
Rectangle().foregroundColor(.gray)
}
.indicator(.activity)
.onAppear(perform: loadImageFromFirebase)
}
func loadImageFromFirebase() {
let storage = Storage.storage().reference(withPath: "images/\(recipeVM.recipe.id ?? "").jpg")
storage.downloadURL { url, error in
if error != nil {
print((error?.localizedDescription)!)
return
}
self.imageURL = url!
}
}
}
For me it looks like the problem is, that the "onAppear" method of the ImageLoaderView is not loaded when coming back from the sheet. So I somehow have to trigger the "loadImageFromFirebase" method whenever the value of "recipeVM" changes. I tried using "didSet" with the var, but this did not work either.
Any ideas?
It would be helpful to see RecipeViewModel, but I'm going to make an assumption that it looks something like this:
struct Recipe {
var id : UUID
}
class RecipeViewModel : ObservableObject {
#Published var recipe : Recipe = Recipe(id: UUID())
}
(Keep in mind that recipe has to be #Published for this to work`)
In that case, the you can listen for changes on recipe by doing something like this in your ImageLoaderView:
.onReceive(recipeVM.$recipe) { _ in
loadImageFromFirebase()
}
This can replace your onAppear, since it'll get called with the initial value and then again any time it changes.
If you wanted to, you could even refactor this to send the specific recipe into the loadImageFromFirebase function:
.onReceive(recipeVM.$recipe) { recipe in
loadImageFromFirebase(recipe: recipe)
}
//....
func loadImageFromFirebase(recipe: Recipe) {
let storage = Storage.storage().reference(withPath: "images/\(recipe.id ?? "").jpg")
storage.downloadURL { url, error in
if error != nil {
print((error?.localizedDescription)!)
return
}
self.imageURL = url!
}
}

MongoDB Realm ObservableObject, SwiftUI?

I'm new to SwiftUI, so please bear with my mistakes.
I'm trying to follow this tutorial, but instead of downloading a JSON, I'm using a MongoDB Realm, specifically Realm.asyncOpen. Here is my ObservableObject class:
class Gallery: ObservableObject {
#Published var results: Results<tiktoks>
init(results: Results<tiktoks>) {
let user = app.currentUser!
let configuration = user.configuration(partitionValue: user.id)
Realm.asyncOpen(configuration: configuration) { (userRealm, error) in
guard error == nil else {
fatalError("Failed to open realm: \(error!)")
}
print("connected somehow")
self.results = userRealm!.objects(tiktoks.self)
}
}
}
I kind of followed this guide too, as I was getting some errors since I'm not using the Decodable structs from the YouTube tutorial. (edit: as I'm looking more deeper I actually might need it, but I had some trouble with them while trying to use them)
But when I try to get the variable from a different struct, I get this error:
#ObservedObject var gallery = Gallery
Type 'Gallery.Type' cannot conform to 'ObservableObject'; only struct/enum/class types can conform to protocols
What am I doing wrong here? Has anyone got MongoDB Realm working with ObservableObject?
I couldn't get ObservableObject to work, so I just put the MongoDB function in an init() function when my view is rendered.
struct Home: View {
let gallery: Results<Tiktoks>
let user = app.currentUser!
let realm: Realm
init() {
let configuration = user.configuration(partitionValue: user.id)
self.realm = try! Realm(configuration: configuration)
let data = realm.objects(Tiktoks.self)
self.gallery = data
print(data)
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [ // ...
It looks like it also re-renders the view when the database is changed too.