SwiftUI Firestore data doesn't showing on the first load - ios

i connected my app wirh Firestore. Everything works fine exept for my data does not show first time when i launch the app. When i switch between tabs, the data shows. It's accting like [Place] array is appending slowly after my app shows. How to force to show data on the first load? here is code:
class FirebaseManager: ObservableObject {
#Published var places = [Place]()
init() {
fetchData()
}
func fetchData(){
Firestore.firestore().clearPersistence()
let db = Firestore.firestore()
db.collection("places").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
self.places.removeAll()
for i in (snap?.documentChanges)!{
let name = i.document.data()["name"] as! String
let type = i.document.data()["type"] as! String
let desc = i.document.data()["desc"] as! String
let image = i.document.data()["image"] as! String
let loc = i.document.data()["loc"] as! String
let contact = i.document.data()["contact"] as! String
let fol = i.document.data()["followers"] as! String
DispatchQueue.main.async {
self.places.append(Place(id: fol, name: name, type: type, description: desc, image: image, location: loc, contact: contact))
print(name)
}
}
}
}
}
struct topView : View {
#ObservedObject var firebaseMan = FirebaseManager()
var body : some View{
VStack(alignment: .leading, spacing: 15){
Text("Clubs & Places")
.font(.system(size: 25, weight: .regular))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15){
ForEach(0 ..< firebaseMan.places.count) {i in
Image(self.firebaseMan.places[i].image)
I tried with .onAppear{... on view, but that doesn't help too.
Thanks.

Resolved! This code is ok, but in SwiftUI ScrollView has a problem that it's not refreshing observasble object! After ScrollView always check if (your #Published var) =! nil! I hope Apple will resolve this problem soon, in next update.

Related

How do I showcase an image from firebase that's within a map element in SwiftUi?

I'm trying to create a beauty and skincare app in SwiftUi with onboarding tutorial steps on how to apply the products. How do I show the firebase data on my front end?
Here's my code so far to show the steps in the product's DB in TutorialView:
var beautyproduct : Beautyproducts
var body: some View {
VStack{
ForEach(0..<beautyproduct.steps.count, id: \.self){step in
HStack(spacing: 0){
ForEach(boarding){screen in
VStack{
Text(beautyproduct.steps.step1.title)
.font(.custom("DreamAvenue", size: 40))
.foregroundColor(Color("Black"))
.padding(.leading, -170)
.padding(.top, 5)
}
}
}
}
}
}
And here's my code so far in my Model:
self.beautyproducts = documents.map{(queryDocumentSnapshot) -> Beautyproducts in
let data = queryDocumentSnapshot.data()
let steps = data["steps"] as? [String : [String : Any]]
var stepsArray = [Steps]()
if let steps = steps{
for step in steps{
///adding main data instead of step data
let title = step.title as? String ?? ""
let description = step.description as? String ?? ""
let image = step.image as? String ?? ""
stepsArray.append(Steps(title: title, description: description,
image: image))
}
}
}

SwiftUI TabView PageTabViewStyle crashes without showing any error

I am trying to create an Image carousel using tabview and loading pictures from firebase. Without showing any error message or code tabview crashing. Please shed some light on what's going wrong here.
struct HomeView : View{
var body : View{
NavigationView{
VStack{
ScrollView{
CategoryView(homeViewModel: homeViewModel)
PosterView(homeViewModel: homeViewModel)
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitle("")
}
}
}
struct PosterView : View {
#StateObject var homeViewModel : HomeViewModel = HomeViewModel()
#State var currentIndex: Int = 0
var timer = Timer.publish(every: 3, on: .main, in: .common)
func next(){
withAnimation{
currentIndex = currentIndex < homeViewModel.posterList.count ? currentIndex +
1 : 0
}
}
var body: some View{
Divider()
GeometryReader{ proxy in
VStack{
TabView(selection: $currentIndex){
ForEach(homeViewModel.posterList){ item in
let imgURL = homeViewModel.trendingImgDictionary[item.id ?? ""]
AnimatedImage(url: URL(string: imgURL ?? ""))
}
}.tabViewStyle(PageTabViewStyle())
.padding()
.frame(width: proxy.size.width, height: proxy.size.height)
.onReceive(timer) { _ in
next()
}
.onTapGesture {
print("Tapped")
}
}
}
}
}
ViewModel: It contains two methods to fetch data and pictures from Firebase. That's working fine and am getting proper data. The only issue is while displaying it tabview crashes without showing any error messages.
class HomeViewModel : ObservableObject {
#Published var posterList : [TrendingBanner] = []
#Published var trendingImgDictionary : [String : String] = [:]
init() {
self.fetchTrendingList()
}
func fetchTrendingList() {
self.posterList.removeAll()
firestore.collection(Constants.COL_TRENDING).addSnapshotListener { snapshot, error in
guard let documents = snapshot?.documents else{
print("No Documents found")
return
}
self.posterList = documents.compactMap({ (queryDocumentSnapshot) -> TrendingBanner? in
return try? queryDocumentSnapshot.data(as:TrendingBanner.self )
})
print("Trending list \(self.posterList.count)")
print(self.posterList.first?.id)
let _ = self.posterList.map{ item in
self.LoadTrendingImageFromFirebase(id: item.id ?? "")
}
}
}
func LoadTrendingImageFromFirebase(id : String) {
let storageRef = storageRef.reference().child("trending/\(id)/\(id).png")
storageRef.downloadURL { (url, error) in
if error != nil {
print((error?.localizedDescription)!)
return
}
self.trendingImgDictionary[id] = url!.absoluteString
print("Trending img \(self.trendingImgDictionary)")
}
}
If you open SwiftUI module sources, you'll see the comment on top of TabView:
Tab views only support tab items of type Text, Image, or an image
followed by text. Passing any other type of view results in a visible but
empty tab item.
You're using AnimatedImage which is probably not intended to be supported by TabView.
Update
I made a library that liberates the SwiftUI _PageView which can be used to build a nice tab bar. Check my story on that.
Had the same issue recently on devices running iOS 14.5..<15. Adding .id() modifier to the TabView solved it.
Example:
TabView(selection: $currentIndex) {
ForEach(homeViewModel.posterList) { item in
content(for: item)
}
}
.tabViewStyle(PageTabViewStyle())
.id(homeViewModel.posterList.count)

SwiftUI: Writing Values or Key:Value Pairs to JSON Decoded Dictionary

I am struggling with modifying a value in a Dictionary that is made up of data from JSON download data from a php query to SQL. I need to either create a new pair key:value pair (best approach) or reuse a field from the original SQL data that I am not using and rewrite the value in the pair. In the code below I am trying the second approach (rewrite the value in a key:value pair I am not using). The issue is in the getData function (near the bottom) and is noted by the error comment. There is a LocationManager to get the users current location and some extensions that help the search box capability that are not shown, but let me know if needed.
import SwiftUI
import MapKit
struct Response: Codable, Hashable {
var District: String
var BusinessName: String
var LocationAddress: String
var LocationCity: String
func hash(into hasher: inout Hasher) {
hasher.combine(BusinessName)
}
}
struct ContentView: View {
#ObservedObject var lm = LocationManager()
#State var response = [Response]()
#State var search: String = ""
#State private var showCancelButton: Bool = false
#State private var searchPerfromed: Bool = false
#State var location2: CLLocationCoordinate2D?
#State var distance: Double = 0
#State var dist: [String: Double] = Dictionary()
var body: some View {
VStack {
HStack {
HStack {
Image(systemName: "magnifyingglass")
TextField("Search", text: $search, onEditingChanged: { isEditing in
self.showCancelButton = true
}, onCommit: {
self.searchPerfromed = true
getData() // Function to get JSON data executed after search execution
}).foregroundColor(.primary)
Button(action: {
self.search = ""
self.searchPerfromed = false
}) {
Image(systemName: "xmark.circle.fill").opacity(search == "" ? 0 : 1)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary)
.background(Color(.secondarySystemBackground))
.cornerRadius(10.0)
if showCancelButton {
Button("Cancel") {
UIApplication.shared.endEditing(true)
self.search = ""
self.showCancelButton = false
self.searchPerfromed = false
}
}
}
.padding(EdgeInsets(top: 10, leading: 0, bottom: 1, trailing: 0))
.padding(.horizontal)
Spacer()
if searchPerfromed == true {
List {
ForEach(response.sorted {$0.LocationAddress > $1.LocationAddress}, id: \.self) { item in
VStack(alignment: .leading) {
HStack(alignment: .center) {
Text("\(item.BusinessName)").font(.subheadline)
Spacer()
if dist[item.LocationAddress] == 0.0 {
Text("Unknown").font(.caption).foregroundColor(.gray)
} else {
Text("\(dist[item.LocationAddress] ?? 0, specifier: "%.0f") mi")
.font(.caption).foregroundColor(.gray)
}
}
Text("\(item.LocationAddress), \(item.LocationCity)").font(.subheadline)
}
}
}
}
}
}
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate else {
completion(nil)
return
}
completion(location)
}
}
func getDistance() {
let loc1 = CLLocation(latitude: lm.location1!.latitude, longitude: lm.location1!.longitude)
let loc2 = CLLocation(latitude: location2?.latitude ?? lm.location1!.latitude, longitude: location2?.longitude ?? lm.location1!.longitude)
let dist = loc1.distance(from: loc2)
distance = dist * 0.00062 // convert from meters to miles
}
func getData() {
let baseURL = "https://???"
let combinedURL = baseURL + search
let encodedURL = combinedURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: encodedURL)!
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
if let responseData = data {
let decodedData = try JSONDecoder().decode([Response].self, from: responseData)
DispatchQueue.main.async {
self.response = decodedData
print(decodedData)
for item in self.response {
let address = item.LocationAddress
getLocation(from: address) { coordinates in
self.location2 = coordinates
getDistance()
dist[address] = distance
let d = String(format: "%.1f", distance)
print(item.District)
item[District] = d //Error: Cannot find 'District' in scope
}
}
}
} else {
print("No data retrieved")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
Just above the issue I assigned the distance value to a new Dictionary and match the values based on address, but this has limitations with other changes I need to make to get to the end state with the app. I have tried lots of different ways to assign the distance to an existing key:value pair in Response or create a new key value pair in Response. Some examples below:
item.District = d //Error: Cannot assign to property: 'item' is a 'let' constant
item[Distance] = d //Error: Cannot find 'Distance' in scope
item["Distance"] = d //Error: Value of type 'Response' has no subscripts
How do I create a new key:value pair in Response, or assign the value of d to the District key? Thanks so much in advance for any help you can provide.
First thing, your instance variables on your Response struct are a little confusing because they don't follow the convention of having variables in all-lowercase, and types capitalized.
Second, for your first error in your list of 3, item is a let constant because it is inside a for loop. You can get around this by declaring the loop this way:
for var item in self.response {
// I can modify item in this loop because it is declared a var
}
The other two errors are pretty self-explanatory, I think.
Third, it sounds like you want to alter your Response object programmatically after receiving it, which is also a bit of an anti-pattern. If you want to modify an object you have downloaded from a server, that's understandable, but it is confusing for someone reading your code to alter an object called "Response." (once you modify it, it no longer represents the server response for which it is named) At a minimum, you could change District to be a computed property of Response.
All that said, if you instantiate your loop using the var keyword, you should be able to do:
item.District = d

Using .environmentObject with a View presented as a sheet causes onReceive not to fire/conflicts with #EnvironmentObject

In my ContentView, I have a button that does a simple sheet present of my SettingsView. There seems to be some conflict with my #EnvironmentObject var iconSettings: IconNames in SettingsView when the view is presented modally where my onReceive function only fires when the view loads the first time and never when the Picker is used.
Looking around for answers related to this, I was only able to find something related to CoreData which wasn't really helpful but I'm sure others have experienced this, so would be great to have something canonical and more general for others to reference.
Thanks!
Button(action: { self.modalDisplayed = true }) {
Assets.gear
}.sheet(isPresented: $modalDisplayed) {
SettingsView(state: self.state, loadCards: self.load)
.environmentObject(IconNames())
}
Then in my SettingsView, I have the following:
struct SettingsView: View {
#ObservedObject var state: AppState
#EnvironmentObject var iconSettings: IconNames
let loadCards: () -> Void
var body: some View {
VStack {
Picker("", selection: $iconSettings.currentIndex) {
ForEach(Publication.allCases, id: \.pubId) {
Text($0.pubName).tag($0.pubId)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding(20)
Spacer()
}
.onReceive([self.iconSettings.currentIndex].publisher.first()) { value in
print(value) // only hits on first load, never on tap
print("")
let index = self.iconSettings.iconNames.firstIndex(of: UIApplication.shared.alternateIconName) ?? 0
if index != value { UIApplication.shared.setAlternateIconName(self.iconSettings.iconNames[value]) { error in
if let error = error {
print(error.localizedDescription)
} else {
print("Success!")
}
}
}
}
}
}
Finally, my IconNames class:
class IconNames: ObservableObject {
var iconNames: [String?] = [nil]
#Published var currentIndex = 0
init() {
getAlternateIconNames()
if let currentIcon = UIApplication.shared.alternateIconName {
self.currentIndex = iconNames.firstIndex(of: currentIcon) ?? 0
}
}
func getAlternateIconNames() {
if let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
let alternateIcons = icons["CFBundleAlternateIcons"] as? [String: Any]
{
for (_, value) in alternateIcons{
guard let iconList = value as? Dictionary<String,Any> else{return}
guard let iconFiles = iconList["CFBundleIconFiles"] as? [String]
else{return}
guard let icon = iconFiles.first else{return}
iconNames.append(icon)
}
}
}
}
Well, as provided code snapshot is not testable by copy-paste, so only by code reading, try instead of
.onReceive([self.iconSettings.currentIndex].publisher.first()) { value in
use this one
.onReceive(self.iconSettings.$currentIndex) { value in

Inconsistent ContentView updates from #Environment var Changes

I have a simple SwiftUI, CoreData application. The architecture is the basic list with
a second view for viewing the detail or editing the detail. The basic structure seems
to work with one important exception. When editing a record, the first edit after
app start is properly visible after returning to the ContentView list. The second and
further edits do not appear on the list when returning to the ContentView. The database
changes are correctly saved. Restarting the app will display the correct data. I have
also created a TabView. If I disable the return-to-main-list-after-edit code and use
just the TabView to switch, the changes are always presented.
Here's the code (I have removed most of the repetitive data fields).
In SceneDelegate:
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let tabby = TabBar().environment(\.managedObjectContext, managedObjectContext)
window.rootViewController = UIHostingController(rootView: tabby)
In ContentView:
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems: FetchedResults<ToDoItem>
#State private var newToDoItem = ""
#State private var gonnaShow = true
#State private var show = false
var body: some View {
NavigationView {
List {
Section(header: Text("Records")) {
ForEach(self.toDoItems) { toDoItem in
NavigationLink(destination: EditToDo(toDoItem: toDoItem)) {
ToDoItemView(
idString: toDoItem.myID.uuidString,
title: toDoItem.title!,
firstName: toDoItem.firstName!,
lastName: toDoItem.lastName!,
createdAt: self.localTimeString(date: toDoItem.createdAt!)
)
}
}//for each
.onDelete { indexSet in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.onMove(perform: move)
}
}
.navigationBarTitle("Customers")
.navigationBarItems(trailing: EditButton())
}
}
Separate file EditToDo:
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.presentationMode) var presentationMode
var toDoItem: ToDoItem
#State private var updatedTitle: String = "No Title"
#State private var updatedFirstName: String = "No First Name"
//more data fields
#State private var updatedDate: Date = Date()
#State private var updatedDateString: String = "July 2019"
var body: some View {
ScrollView {
VStack {
Image("JohnForPosting")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(Circle())
VStack(alignment: .leading) {
Text("ToDo Title:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a Title", text: $updatedTitle)
.onAppear {
self.updatedTitle = self.toDoItem.title ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
VStack(alignment: .leading) {
Text("First Name:")
.padding(.leading, 5)
.font(.headline)
TextField("Enter a First Name", text: $updatedFirstName)
.onAppear {
self.updatedFirstName = self.toDoItem.firstName ?? ""
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(10)
//more data fields
VStack {
Button(action: ({
self.toDoItem.title = self.updatedTitle
self.toDoItem.firstName = self.updatedFirstName
//more data fields
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
self.updatedTitle = ""
self.updatedFirstName = ""
//more data fields
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Save")
}
.padding()
}
.padding(10)
Spacer()
}
}
}
Separate file ToDoItemView:
var idString: String = ""
var title: String = ""
var firstName: String = ""
//more data fields
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("\(firstName) \(lastName)")
.font(.headline)
Text("\(createdAt) and \(idString)")
.font(.caption)
}
}
}
Xcode 11 - I guess this is the real release (post GM seed 2), Catalina Beta 19A558d,
iOS13.1
I thought the #Environment changes would always cause the body of ContentView to be
redrawn. And it is the weird behavior of first edit working, others not. Any guidance
would be appreciated.
I have the same problem with my current project, same basic code as you as well. A list of Projects fed by a FetchRequest that navigates to a second view for editing a Project.
Like you, if I make changes in the detail view, those changes are reflected in the List, but only the first time. If I reload the List by changing to a different tab and back again, the List will update showing the updated data.
The root of the problem comes down to creating a dynamic list using the results of a #FetchRequest. FetchRequest returns a Set of NSManagedObjects (reference types). Since the List is seeded with a Set of reference types, SwiftUI would only update the view when a reference changed, not necessarily when one of the properties of the object changed.
I think #FetchRequest would be fine for things like Pickers or menu options, but for a dynamic list of TodoItems, a better solution might be to create a TodoManager.
The TodoManager would be the single source of truth for all TodoItems. It would also setup a NSManagedObjectContextObserver that would trigger a objectWillChange.send() notification when any change was made, thus signaling that SwiftUI should refresh the list.
class TodoManager: ObservableObject {
private var moc: NSManagedObjectContext
public let objectWillChange = PassthroughSubject<Void, Never>()
var todoList: [TodoItem] = []
init( context: NSManagedObjectContext ) {
moc = context
setupNSManagedObjectContextObserver( context: context )
todoList = Array( TodoItem.fetch(in: context ))
}
func setupNSManagedObjectContextObserver( context moc: NSManagedObjectContext ) {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(managedObjectContextObjectsDidChange), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextWillSave), name: NSNotification.Name.NSManagedObjectContextWillSave, object: moc)
notificationCenter.addObserver(self, selector: #selector(managedObjectContextDidSave), name: NSNotification.Name.NSManagedObjectContextDidSave, object: moc)
}
#objc func managedObjectContextObjectsDidChange(notification: NSNotification) {
self.objectWillChange.send() // Crude but works
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
#objc func managedObjectContextWillSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
#objc func managedObjectContextDidSave(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 { }
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 { }
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 { }
}
}
TodoManager is pretty dumb in that it sends an objectWillChange notification anytime any data in the ManagedObjectContext is changed.
Steve
Xcode 11.1 GM seed resolves this issue. For what it's worth, the Preview function for the core data fed list still does not work.

Resources