SwiftUI: How to set UserDefaults first time view renders? - ios

So I have this code, where I fetch a url from firestore and then append it to an array, which is then stored in userDefaults(temporarily).
In the view I basically just iterate over the array stored in userdefaults and display the images.
But the problem is, that I have to rerender the view before the images show.
How can i fix this?
struct PostedImagesView: View {
#State var imagesUrls : [String] = []
#ObservedObject var postedImagesUrls = ProfileImages()
var body: some View {
VStack{
ScrollView{
ForEach(postedImagesUrls.postedImagesUrl, id: \.self) { url in
ImageWithURL(url)
}
}
}
.onAppear{
GetImage()
print("RAN GETIMAGE()")
}
}
// Get Img Url from Cloud Firestore
func GetImage() {
guard let userID = Auth.auth().currentUser?.uid else { return }
let db = Firestore.firestore()
db.collection("Users").document(userID).collection("PostedImages").document("ImageTwoTester").getDocument { (document, error) in
if let document = document, document.exists {
// Extracts the value of the "Url" Field
let imageUrl = document.get("Url") as? String
UserDefaults.standard.set([], forKey: "postedImagesUrls")
imagesUrls.append(imageUrl!)
UserDefaults.standard.set(imagesUrls, forKey: "postedImagesUrls")
} else {
print(error!.localizedDescription)
}
}
}
}

Related

SwiftUI Navigation - List loading multiple time after navigating from details

I am creating a SwiftUI List with Details.
This list is fetching JSON data from Firebase Realtime. The data consist of 5 birds with an ID, a name and an image URL.
My problem is the following:
Each time I click on the back button after I navigate to details, the data get doubled every single time, what am I doing wrong? (see screenshots).
I am using MVVM design pattern, I am listening and removing that listener every time the View appears and disappears.
Please, find the code below:
Main View:
var body: some View {
NavigationStack {
List(viewModel.birds) { bird in
NavigationLink(destination: DetailsView(bird: bird)) {
HStack {
VStack(alignment: .leading) {
Text(bird.name).font(.title3).bold()
}
Spacer()
AsyncImage(url: URL(string: bird.imageURL)) { phase in
switch phase {
// downloading image here
}
}
}
}
}.onAppear {
viewModel.listentoRealtimeDatabase()
}
.onDisappear {
viewModel.stopListening()
}.navigationTitle("Birds")
}
}
DetailsView:
struct DetailsView: View {
var bird: Bird
var body: some View {
Text("\(bird.name)")
}
}
Model:
struct Bird: Identifiable, Codable {
var id: String
var name: String
var imageURL: String
}
View Model:
final class BirdViewModel: ObservableObject {
#Published var birds: [Bird] = []
private lazy var databasePath: DatabaseReference? = {
let ref = Database.database().reference().child("birds")
return ref
}()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
func listentoRealtimeDatabase() {
guard let databasePath = databasePath else {
return
}
databasePath
.observe(.childAdded) { [weak self] snapshot in
guard
let self = self,
var json = snapshot.value as? [String: Any]
else {
return
}
json["id"] = snapshot.key
do {
let birdData = try JSONSerialization.data(withJSONObject: json)
let bird = try self.decoder.decode(Bird.self, from: birdData)
self.birds.append(bird)
} catch {
print("an error occurred", error)
}
}
}
func stopListening() {
databasePath?.removeAllObservers()
}
}
screenshot how it should be

getting documents from a subcollection Firestore swiftUI

I'm trying to retrieve data from documents in a subCollection in Firestore, but it returns an empty array after executing it.
I tried to do the following.
#Published var OrdersID = [String]()
func fetchOrdersID(){
FirebaseManager.shared.firestore.collection("Users").document(id).collection("OrdersID").getDocuments { snapshot, error in
guard let documents = snapshot?.documents else {
print("No documents")
return
}
DispatchQueue.main.async {
self.OrdersID = documents.map{ d in
return d["id"] as? String ?? ""
}
}
}
}
but still it returns an empty array of OrdersID outside this function, and inside of it returns the proper array list(IMAGE 1 shows this, orders ID is the content of the OrdersID array IMAGE 1)
You could try escaping with a completion..
#Published var OrdersID = [String]()
func fetchOrdersID(withCompletion completion: #escaping ((_ orderID: String) -> (Void))) {
FirebaseManager.shared.firestore.collection("Users").document(id).collection("OrdersID").getDocuments { snapshot, error in
guard let documents = snapshot?.documents else {
print("No documents")
return
}
let orderID = documents["id"] as? String ?? ""
completion(orderID)
}
}
And then when you go to call, you can add to the OrdersId...
fetchOrdersID { orderID in
OrdersID.append(orderID)
}

SwiftUI list not updating when the array changes

VStack(spacing: 0){
List{
ForEach(postsData.fetchedPosts, id: \.postID) { post in
SocialPostView(post: post, showAccount: self.$showAccount, fetchedUser: self.$fetchedUser)
.padding(.vertical)
.listRowInsets(EdgeInsets())
.onAppear {
self.elementOnAppear(post)
}
}
}
.pullToRefresh(isShowing: $isShowing) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isShowing = false
self.postsData.newFetch = true
self.postsData.fetchPosts(userInfo: self.userInfo)
}
}
}
private func elementOnAppear(_ post: Post) {
/* onAppear on the view is called when a view appears on screen.
elementOnAppear asks the view model if the element is the last one.
If so, we ask the view model to fetch new data. */
if self.postsData.isLastPostInList(post: post) {
self.postsData.fetchPosts(userInfo: self.userInfo)
}
}
When each list element appears, it checks if it's the last element in the array. If it is, it fetches more from Firestore and updates fetchedPosts. However, when the array updates, the List is not updated, so no new elements show up.
This is the ObservableObject, which publishes the array.
class SocialObservable: ObservableObject{
let db = Firestore.firestore()
let objectWillChange = ObservableObjectPublisher()
#Published var fetchedPosts = [Post]()
#Published var lastSnap : DocumentSnapshot?
#Published var reachedEnd = false
var currentListener: ListenerRegistration?
var newFetch = false {
willSet{
objectWillChange.send()
}
}
init(userInfo: UserData){
print(userInfo.uid)
fetchPosts(userInfo: userInfo)
}
func fetchPosts(userInfo: UserData){
var first: Query?
if lastSnap == nil || newFetch {
//not last snapshot, or just updated feed
if newFetch{
newFetch.toggle()
fetchedPosts = [] // clean up if new fetch
}
first = db.collection("posts")
.whereField("availability", arrayContains: userInfo.uid)
.order(by: "date", descending: true)
.limit(to: 1)
}
else {
first = db.collection("posts")
.whereField("availability", arrayContains: userInfo.uid)
.order(by: "date", descending: true)
.start(afterDocument: lastSnap!)
.limit(to: 1)
}
first?.getDocuments(completion: { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error: \(error.debugDescription)")
return
}
let doc = snapshot.documents.map({postFromDB(obj: $0.data(), id: $0.documentID)})
doc.map({print($0.postID)})
// append to fetched posts
self.fetchedPosts = self.fetchedPosts + doc
print(self.fetchedPosts.count)
//prepare for the next fetch
guard let lastSnapshot = snapshot.documents.last else {
// the collection is empty. no data fetched
self.reachedEnd = true
return
}
// save last snapshot
self.lastSnap = lastSnapshot
})
}
func isLastPostInList(post: Post) -> Bool {
return post.postID == fetchedPosts.last?.postID
}
}
Is there any workaround for this?
A couple of things
class SocialObservable: ObservableObject{
let db = Firestore.firestore()
// let objectWillChange = ObservableObjectPublisher() // << remove this
// ...
var newFetch = false {
willSet{
self.objectWillChange.send() // ObservableObject has default
}
}
// ...
and on update modify published on main queue
doc.map({print($0.postID)})
// append to fetched posts
DispatchQueue.main.async {
self.fetchedPosts = self.fetchedPosts + doc
}
print(self.fetchedPosts.count)

Escaping closure captures mutating 'self' parameter, Firebase

I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}
SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}
Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}

Swift Firestore Get Document ID

I'm using FirebaseFirestore for my database.
I have an array of Lists with a name and description. I'd also like to grab each lists unique documentId. Is this possible?
List.swift
struct List {
var name:String
var description:String
var dictionary:[String:Any] {
return [
"name":name,
"description":description
]
}
}
ListTableViewController.swift
func getLists() {
if Auth.auth().currentUser != nil {
db.collection("lists").getDocuments { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
self.listArray = querySnapshot!.documents.flatMap({List(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
I found you can get the documentId by doing...
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
But how do I store this in my List to be able to call later?
1.Suppose you are getting documents in snapshot.
guard let documents = ducomentSnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
// Get documentId of each document objects using this code.
for i in 0 ..< documents.count {
let dictData = documents[i].data()
let documentID = documents[i].documentID
print("Document ID \(documentID)")
}
I'm not positive if this is the best way but I appended to the listArray and it seems to work.
self.listArray.append(List(name: document["name"] as! String, description: document["description"] as! String, documentId: document.documentID))
Add an id attribute to your list struct:
struct List {
var name:String
var description:String
var id: String?
var dictionary:[String:Any] {
return [
"name":name,
"description":description
]
}
}
Then when mapping your documents manually assign the id attribute:
list = documents.compactMap{ (queryDocumentSnapshot) -> List? in
var listItem = try? queryDocumentSnapshot.data(as: List.self)
listItem?.id = queryDocumentSnapshot.documentID
return listItem
}

Resources