I am working on a SwiftUI app and using Firebase Firestore as a backend. I am noticing an odd behavior when I update a document in Firestore that results in a view being popped off the Navigation stack each time a field is updated. The Navigation Stack is as follows.
MarketplaceView with List Containing ListingRowViews
Tapping a row takes you to a ListingDetailView.
Tapping a Buy Now button in ListingDetailView takes you to a ConfirmationView.
When updates are made to the document and 1 or 2 is present, the view updates and all behaves as expected. When 3 (ConfirmationView) is present, any updates to the document result in the ConfirmationView being popped off the stack back to 2 the ListingDetailView. Below is the app architecture.
ListingRepository - Creates a snapshotListener for all Listings in Firestore.
class ListingRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#Published var listings = [Listing]()
private var cancellables = Set<AnyCancellable>()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
// Add a SnapshotListener to the Listing Collection.
self.snapshotListener = db.collection(FirestoreCollection.listings).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
// Check to make sure the Collection contains Documents
guard let documents = querySnapshot?.documents else {
print("No Listings.")
return
}
// Documents exist.
self.listings = documents.compactMap { listing in
do {
return try listing.data(as: Listing.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
MarketplaceViewModel - Subscribes to Listings from ListingRepository and creates ListingRowViewModels.
class MarketplaceViewModel: ObservableObject {
// Properties
#Published var listingRepository: ListingRepository = Resolver.resolve()
// Published Properties
#Published var listingRowViewModels = [ListingRowViewModel]()
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// Intitalizer
init() {
self.startCombine()
}
// Starting Combine
func startCombine() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings
.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
MarketplaceView - Creates the List full of ListingDetailViews
struct MarketplaceView: View {
#ObservedObject var marketplaceViewModel: MarketplaceViewModel = Resolver.resolve()
var body: some View {
return NavigationView {
List {
ForEach(self.marketplaceViewModel.listingRowViewModels, id: \.id) { listingRowViewModel in
NavigationLink(destination: ListingDetailView(listingDetailViewModel: ListingDetailViewModel(listing: listingRowViewModel.listing))
) {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
} // ForEach
.navigationTitle("Marketplace")
} // NavigationView
} // View
}
}
ListingRowViewModel - View model for each row.
class ListingRowViewModel: ObservableObject, Identifiable {
// Properties
var id: String = ""
// Published Properties
#Published var listing: Listing
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// Initializer
init(listing: Listing) {
self.listing = listing
self.startCombine()
}
// Starting Combine
func startCombine() {
$listing
.receive(on: RunLoop.main)
.compactMap { listing in
listing.id
}
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
}
ListingRowView - View for each row.
struct ListingRowView: View {
#ObservedObject var listingRowViewModel: ListingRowViewModel
var body: some View {
Text(self.listingRowViewModel.listingId)
} // View
}
ListingDetailViewModel - View model for the detail view.
class ListingDetailViewModel: ObservableObject, Identifiable {
var listing: Listing
// Initializer
init(listing: Listing) {
self.listing = listing
}
}
ListingDetailView - Detail View For Listing
struct ListingDetailView: View {
var listingDetailViewModel: ListingDetailViewModel
var body: some View {
VStack {
Text(self.listingDetailViewModel.listing.id)
NavigationLink(destination: ConfirmationView(confirmationViewModel: ConfirmationViewModel(listing: listing))) {
Text("Buy Now")
}
}
.navigationTitle("Listing Info")
} // View
}
ConfirmationViewModel - View model for Confirmation View
class ConfirmationlViewModel: ObservableObject, Identifiable {
var listing: Listing
// Initializer
init(listing: Listing) {
self.listing = listing
}
}
ConfirmationView - Confirmation View
struct ConfirmationView: View {
var confirmationViewModel: ConfirmationViewModel
var body: some View {
VStack {
Text(self.confirmationViewModel.listing.id)
Button(action: {
self.confirm()
}, label: {
Text("Confirm")})
}
.navigationTitle("Order Confirmation")
} // View
}
Any help would be greatly appreciated.
Related
In my content view I have a home page with some text that says "Welcome, xxxx" where xxxx is the name fetched from a firebase database. This field can be changed in the settings page that is navigated to via a Navigation Link. When the name is changed and saved the name on the home page only updates when you force shutdown the app. How do I force update the view when you press the back button from settings.
This is how I display the field:
Text("Welcome, \(companyName)")
.font(.system(size: 23))
.bold()
.foregroundColor(Color("background"))
.padding(.bottom, 50)
This is how I set a value to companyName:
func SetData() {
var db = Firestore.firestore()
let user = Auth.auth().currentUser
let userName = user?.email ?? ""
let docRef = db.collection("CONTACT").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.companyName = data?["companyName"] as? String ?? ""
} else {
print("Document does not exist")
}
}
}
There are several solutions to this, but you haven't provided enough code outlining what you have done to modify the variable companyName. The easiest solution would be to pass companyName as a binding value into the settings.
What I imagine here is that your HomeView is fetching the data on launch. In the settings, a change data request is made, but nothing is done to update the data in the HomeView. By using a binding variable we can ensure that the companyName connects to the source of truth in the HomeView, and so the function modifies the companyName which is precisely the company name on the HomeView vs. modifying potentially the value of companyName.
struct HomeView: View {
#State var companyName = "Microsoft"
var body: some View {
NavigationView {
NavigationLink(destination: SettingsView(companyName: $companyName)) {
Text("Tap to navigate to Settings")
}
}
}
}
struct SettingsView: View {
#Binding var companyName : String
var body: some View {
Button {
SetData()
} label: {
HStack {
Text("Tap to change!")
Text("\(companyName)!")
}
}
}
func SetData() {
var db = Firestore.firestore()
let user = Auth.auth().currentUser
let userName = user?.email ?? ""
let docRef = db.collection("CONTACT").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.companyName = data?["companyName"] as? String ?? ""
} else {
print("Document does not exist")
}
}
}
}
If you have already done this at it doesn't somehow work, another solution is to add an .onAppear modifier to your HomeView.
struct HomeView: View {
#State var companyName = "Microsoft"
var body: some View {
VStack {
// code ...
}
.onAppear {
fetchData()
}
}
func fetchData() {
// code that returns companyFetchedName
self.companyName = companyFetchedName
}
}
Modify it on main queue, like
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
DispatchQueue.main.async { // << here !!
self.companyName = data?["companyName"] as? String ?? ""
}
} else {
print("Document does not exist")
}
}
I am trying to use Firestore and get the data from the Firestore and then put it in EnvironmentObject. But it is not working out. I tried different approaches each ended with different errors.
Here is my code:
class FirebaseManager: ObservableObject {
#Published var tweets: [Tweet] = []
private let db: Firestore = Firestore.firestore()
init() {
db.collection("tweets")
.addSnapshotListener { snapshot, error in
if let error {
print(error.localizedDescription)
return
}
let tweets = snapshot?.documents.compactMap({ document in
try? document.data(as: Tweet.self)
})
if let tweets {
DispatchQueue.main.async {
self.tweets = tweets
}
}
}
}
}
Then I try to call FirebaseManager in the code below:
struct HomeTimelineScreen: View {
#EnvironmentObject var appState: AppState
#StateObject private var firebaseManager = FirebaseManager()
private var cancellables = Set<AnyCancellable>()
init() {
// ERROR Escaping closure captures mutating 'self' parameter
firebaseManager.$tweets.sink { tweets in
appState.tweets = tweets
}
// ERROR/WARNING ObservableObject of type AppState found. A View.environmentObject(_:) for AppState may be missing as an ancestor of this view.
firebaseManager.$tweets.assign(to: \.appState.tweets, on: self)
.store(in: &cancellables)
}
The EnvironmentObject AppState is injected in the TwitterApp main file as shown below:
#main
struct TwitterAppApp: App {
#ObservedObject var coordinator = Coordinator()
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
NavigationStack(path: $coordinator.path) {
LandingScreen()
.navigationDestination(for: Route.self) { route in
switch route {
case .login:
LoginScreen().appLogoToolbar()
case .register:
RegistrationScreen().appLogoToolbar()
case .home:
HomeScreen()
case .detail(let tweet):
TweetDetailsScreen(tweet: tweet)
}
}
}.environmentObject(coordinator)
.environmentObject(AppState())
}
}
}
AppState:
import Foundation
class AppState: ObservableObject {
#Published var tweets: [Tweet] = []
}
UPDATE: My biggest issue is on how to re-render the TweetDetailScreen since it is also using the TweetCellView.
struct TweetDetailsScreen: View {
let tweet: Tweet
var body: some View {
List {
TweetCellView(tweet: tweet)
ForEach(1...20, id: \.self) { index in
Text("\(index)")
}
}
}
}
I took your advice and added single source of truth FirebaseManager and now it works as I want it to be.
struct TweetDetailsScreen: View {
#EnvironmentObject var firebaseManager: FirebaseManager
let tweet: Tweet
var body: some View {
List {
if let tweet = firebaseManager.findByDocumentId(tweet.documentID ?? "") {
TweetCellView(tweet: tweet)
}
ForEach(1...20, id: \.self) { index in
Text("\(index)")
}
}
}
}
class FirebaseManager: ObservableObject {
#Published var tweets: [Tweet] = []
private let db: Firestore = Firestore.firestore()
init() {
db.collection("tweets")
.addSnapshotListener { snapshot, error in
if let error {
print(error.localizedDescription)
return
}
let tweets = snapshot?.documents.compactMap({ document in
try? document.data(as: Tweet.self)
})
if let tweets {
DispatchQueue.main.async {
self.tweets = tweets
}
}
}
}
func findByDocumentId(_ documentId: String) -> Tweet? {
guard let index = tweets.firstIndex(where: { $0.documentID == documentId }) else { return nil }
return tweets[index]
}
}
I am using an ObservedObject with a singleton which is populated via an API call to present part of my view. However, when I update my singleton, the view isn't updated accordingly. Here is my view code:
struct Friends: View {
#State private var friends: [FirestoreUser] = []
#State private var loading = false
#ObservedObject private var currentUser = CurrentUser.shared // singleton
// Bunch of UI code ...
GeometryReader { subGeo in
VStack {
List {
ForEach(friends, id: \.id) {
UserItem(user: $0)
.background(Color.white)
}
.listRowBackground(Color.white)
}
.listRowBackground(Color.white)
Spacer()
// Some brackets and then ...
.onReceive(self.currentUser.$user, perform: { _ in
getFriends()
})
Here's the getFriends() function:
func getFriends() {
print("in get friends func")
if let friendUIDs = currentUser.user?.firestoreFriendUIDs {
print("getting friends for friends page")
if friendUIDs.count > 0 {
self.loading = true
FirestoreService.fetchUsers(uids: friendUIDs) { (friends) in
self.friends = friends!
self.loading = false
}
}
}
}
Here is my CurrentUser class:
import Foundation
import SwiftUI
class CurrentUser: ObservableObject {
#Published var user: FirestoreUser?
public static var shared = CurrentUser()
func setUser(user: FirestoreUser) {
self.user = user
}
}
And here is where I'm initially setting the singleton:
func listen() {
handle = auth.addStateDidChangeListener({ (auth, user) in
if let authUser = user {
print("user in addStateDidChangeListener is not nil")
self.loggedInFIRAuthUser = user
FirestoreService.fetchUser(uid: authUser.uid) { (user) in
DispatchQueue.main.async {
if let user = user {
CurrentUser.shared.setUser(user: user)
print("set CurrentUser singleton")
}
}
}
} else {
print("nil authUser")
}
})
}
This is part of the initial view for the app. What's happening based on print messages is that the program fetches user data from Firestore when it starts up, confirms the user has friends, but onReceived is called just before the singleton is set rather than after. I even tried adding a delay, and the singleton is actually waiting for the .OnReceived code to run. The order of the printed lines is:
in get friends func
user in addStateDidChangeListener is not nil
friendUIDs.count: 1
in get friends func
set CurrentUser singleton
Any help is greatly appreciated, I've been stuck on this for days.
I have a Game-object that may hold an image. Whenever an image URL is found for a game a new instance of GameImage-object should be created. It will then fetch the image and populate the UIImage property. When this happens the UI should be updated presenting the image.
class Game: ObservableObject {
#Published var image: GameImage?
}
class GameImage: ObservableObject {
let url: URL
#Published var image: UIImage?
private var cancellable: AnyCancellable?
init(url: URL) {
self.url = url
}
func fetch() {
self.cancellable = URLSession.shared.dataTaskPublisher(for: self.url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] (image) in
guard let self = self else { return }
self.image = image
print(self.url)
print(self.image)
})
}
func cancel() {
cancellable?.cancel()
}
deinit {
cancel()
}
}
struct ContentView: View {
#StateObject var game = Game()
var body: some View {
VStack {
if let image = game.image?.image {
Image(uiImage: image)
} else {
Text("No image.")
}
}
.onAppear(perform: {
guard let gameImageURL = URL(string: "https://cf.geekdo-images.com/itemrep/img/oVEpcbtyWkJjIjk1peTJo6hI1yk=/fit-in/246x300/pic4884996.jpg") else { return }
game.image = GameImage(url: gameImageURL)
game.image!.fetch()
})
}
}
The problem is. After fetch is done the debug console will show that image contains an UIImage. However the UI does not update to show the image. What am I missing here?
There is much more simpler solution than chaining ObservableObject, just separate dependent part into standalone subview... and all will work automatically.
Here is possible approach. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#StateObject var game = Game()
var body: some View {
VStack {
if nil != game.image {
GameImageView(vm: game.image!)
}
}
.onAppear(perform: {
guard let gameImageURL = URL(string: "https://cf.geekdo-images.com/itemrep/img/oVEpcbtyWkJjIjk1peTJo6hI1yk=/fit-in/246x300/pic4884996.jpg") else { return }
game.image = GameImage(url: gameImageURL)
game.image!.fetch()
})
}
}
struct GameImageView: View {
#ObservedObject var vm: GameImage
var body: some View {
if let image = vm.image {
Image(uiImage: image)
} else {
Text("No image.")
}
}
}
Can someone tell me what I am doing wrong? I am using Swiftui and firebase database. I am not seeing any error or any data on the screen. I did install the Pods and checked the security rules as well in console. I tried couple other methods, but this was exactly same from youtube tutorials except the collection name and fields.
import SwiftUI
import Firebase
struct Calories: View {
#ObservedObject var data = getData()
var body: some View {
NavigationView{
ZStack(alignment: .top){
GeometryReader{_ in
// Home View....
Text("Home")
}.background(Color("Color").edgesIgnoringSafeArea(.all))
CustomSearchBar(data: self.$data.datas).padding(.top)
}.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
struct Calories_Previews: PreviewProvider {
static var previews: some View {
Calories()
}
}
struct CustomSearchBar : View {
#State var txt = ""
#Binding var data : [dataType]
var body : some View{
VStack(spacing: 0){
HStack{
TextField("Search", text: self.$txt)
if self.txt != ""{
Button(action: {
self.txt = ""
}) {
Text("Cancel")
}
.foregroundColor(.black)
}
}.padding()
if self.txt != ""{
if self.data.filter({$0.item.lowercased().contains(self.txt.lowercased())}).count == 0{
Text("No Results Found").foregroundColor(Color.black.opacity(0.5)).padding()
}
else{
List(self.data.filter{$0.item.lowercased().contains(self.txt.lowercased())}){i in
NavigationLink(destination: Detail(data: i)) {
Text(i.item)
}
}.frame(height: UIScreen.main.bounds.height / 5)
}
}
}.background(Color.white)
.padding()
}
}
class getData : ObservableObject{
#Published var datas = [dataType]()
init() {
let db = Firestore.firestore()
db.collection("HSCal").getDocuments { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documents{
let id = i.documentID
let item = i.get("item") as! String
let cal = i.get("cal") as! String
self.datas.append(dataType(id: id, item: item, cal: cal))
}
}
}
}
struct dataType : Identifiable {
var id : String
var item : String
var cal : String
}
struct Detail : View {
var data : dataType
var body : some View{
Text(data.item)
}
}
did you put app bundle?
try in
struct Calories: View {
#EnvironmentObject var List: getData()
....
}
call
Calories().environmentObject(DataList)
declare somewhere
var DataList = getData()