SwiftUI list not updating when the array changes - ios

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)

Related

Trying to save data in UserDefaults and show them in list view

Trying to save some data in UserDefaults but I'm getting nil in the view.
I don't know where is the problem
This is my code in ContentView:
var saveButton: some View {
Button("Save Meal") {
let meal = Meal(name: self.mealGenerator.currentMeal!.name,
imageUrlString: self.mealGenerator.currentMeal!.imageUrlString,
ingredients: self.mealGenerator.currentMeal!.ingredients,
instructions: self.mealGenerator.currentMeal!.instructions,
area: self.mealGenerator.currentMeal!.area,
category: self.mealGenerator.currentMeal!.category)
self.savedMeals.meals.append(meal)
self.savedMeals.saveMeals()
}
This is my class I'm trying to save:
class SavedMeals: ObservableObject {
#Published var meals: [Meal]
func saveMeals() {
if let encoded = try? JSONEncoder().encode(meals) {
UserDefaults.standard.set(encoded, forKey: "Meals")
}
}
init() {
if let meals = UserDefaults.standard.data(forKey: "Meals") {
if let decoded = try? JSONDecoder().decode([Meal].self, from: meals) {
self.meals = decoded
return
}
}
self.meals = []
}
}
And I'm trying to list in a view:
struct SavedMealsView: View {
#ObservedObject var savedMeals: SavedMeals
var body: some View {
NavigationView {
List(savedMeals.meals) { meal in
Text(meal.name)
}
.navigationBarTitle("Saved Meals", displayMode: .inline)
}
}
}
You do meals = [] at the end of your init regardless of what you decode. Perhaps this will work better:
init() {
if data = UserDefaults.standard.data(forKey: "Meals") {
do {
meals = try JSONDecoder().decode([Meal].self, from: meals)
} catch {
assertionFailure("Oops!")
meals = []
}
} else {
meals = []
}
}

SwiftUI: How to set UserDefaults first time view renders?

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)
}
}
}
}

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)")
}
}
//...
}

removing from array is not calling set

I have a list
List {
ForEach (appState.foo.indices, id: \.self) { fooIndex in
Text(foo[fooIndex].name)
}
.onDelete(perform: self.deleteRow)
}
with a function that deletes a row from the foo array:
private func deleteRow(at indexSet: IndexSet) {
self.appState.foo.remove(atOffsets: indexSet)
}
and an observable object that acts as an environment object in the view with the list:
class AppState: ObservableObject {
var foo: [Bar] {
set {
if let encoded = try? JSONEncoder().encode(newValue) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "foo")
}
objectWillChange.send()
self.myFunc()
}
get {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
return loadedTraining
}
return []
}
}
// ....
func myFunc() {
print("I'm not printing when you delete a row")
}
}
How can I get my myFunc() triggered when I delete a row?
Use stored property instead of a computed property. To fix your issue modify the foo property in AppState like this:
struct Bar: Encodable { }
class AppState: ObservableObject {
var foo: [Bar] {
didSet {
if let encoded = try? JSONEncoder().encode(foo) {
UserDefaults.standard.set(encoded, forKey: "foo")
}
objectWillChange.send()
myFunc()
}
}
init() {
if let data = UserDefaults.standard.data(forKey: "foo"),
let savedFoo = try? JSONDecoder().decode([Bar].self, from: data) {
foo = savedFoo
} else {
foo = []
}
}
func myFunc() {
print("I'm not printing when you delete a row")
}
}

Updating swiftui text view after parsing json data

I have a function that goes out to an api and gets a stock price. I have the function returning a double and I can see the correct price print out to the console when I run the code. When I try to set that price to a variable inside the function so the function can return it, I just get 0.000 in the text view. Can someone tell me what I'm doing wrong? My code is below.
import SwiftUI
struct ListView: View {
var body: some View {
List {
HStack {
Text("Stock Price (15 Min. delay)")
Spacer()
Text("\(getStockPrice(stock: symbol))")
}
}
}
func getStockPrice(stock: String) -> Double {
var thePrice = Double()
guard let url = URL(string: "my url to get data") else {
fatalError("URL does not work!")
}
URLSession.shared.dataTask(with: url) { jsonData, _, _ in
guard let jData = jsonData else {return}
do {
if let json = try JSONSerialization.jsonObject(with: jData, options: []) as? [String: Any] {
if let pricer = json["latestPrice"] as? Double {
print(pricer)
thePrice = pricer
}
}
} catch let err {
print(err.localizedDescription)
}
}.resume()
return thePrice
}
}
You can update your UI with changing the #State variable, like in code below:
struct UpdatingPriceAsync: View {
#State var stockPrice: Double = 0.0
var body: some View {
List {
HStack {
Text("Stock Price (15 Min. delay)")
Spacer()
Text("\(stockPrice)")
.onAppear() {
self.updateStockPrice(stock: "something")
}
}
}
}
private func updateStockPrice(stock: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // sort of URL session task
DispatchQueue.main.async { // you need to update it in main thread!
self.stockPrice = 99.9
}
}
}
}

Resources