Access Data from Firestore and Put it in SwiftUI EnvironmentObject - firebase

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

Related

Generic parameter 'Success' could not be inferred; Key path value type '_' cannot be converted to contextual type '_'; Cannot find 'Response' in scope

I think this might be the last push for my project of mobile GitHub query repository search, but I get 3 errors I cannot find out how to cope with.
The code:
import SwiftUI
import Combine
struct Root: Codable {
let items: [Item]
enum CodingKeys: String, CodingKey {
case items
}
}
struct Item: Identifiable, Codable {
let id: Int
let urlCode: String
let fullName: String
enum CodingKeys: String, CodingKey {
case id
case urlCode = "url"
case fullName = "full_name"
}
}
private final class ContentViewState: ObservableObject {
#Published var isLoading = false
#Published var query = ""
#Published var stuff = [String]()
private var subscription: AnyCancellable?
func fetchRepos(query: String) {
isLoading = true
subscription = Just("test")
.delay(for: 2, scheduler: RunLoop.main)
.sink(receiveValue: {[weak self] (title: String) in
self?.isLoading = false
self?.stuff.append(title)
})
}
}
struct ContentView: View {
#StateObject private var state = ContentViewState()
#State private var items = [Item]()
var body: some View {
VStack {
if state.isLoading {
ProgressView()
} else {
HStack {
TextField("Enter search", text: $state.query)
Button("Search") {
state.fetchRepos(query: state.query)
}
}
List(items, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.fullName).font(.headline)
Text(item.urlCode)
}
}.task {
await loadData()
}
}
}
}
func loadData() async {
guard let url = URL(string: "https://api.github.com/search/repositories?q=" + state.query + "&per_page=20") else
{
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Root.self, from: data) {
items = decodedResponse.items
}
} catch {
print("Invalid data ")
}
}
}
The errors:
"Generic parameter 'Success' could not be inferred" on line:
TextField("Enter search", text: $state.query)
"Key path value type '' cannot be converted to contextual type ''" on line:
await loadData()
}
}
"Cannot find 'Response' in scope" on line:
} catch {
print("Invalid data ")
}
}
}
Please help :)
The answer was to move the code to another file, change its structure a bit and everything works fine now!

Updates To Firebase Firestore Document Popping View Off Navigation Stack SwiftUI

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.

How to chain ObservableObject?

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

Data not showing in Swiftui using Firebase

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

How do I load data using ObservableObject in SwiftUI?

I am trying to transition an app from UIKit to SwiftUI which depends on basic DynamoDB resources but I have hit a snag in forcing the view to refresh as data is added to the list. I have been at this set of code for hours trying different things and I thought I might see if anyone might know why the 'SessionsData' seems to be thrown away and will not accumulate the 'Sessions' objects.
Does anyone have any quick thoughts???
class SessionsData: ObservableObject {
let didChange = PassthroughSubject<SessionsData, Never>()
#Published var data: [Sessions] = [] {
didSet {
didChange.send(self)
}
}
init() {
load()
}
func load() {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
var temp : [Sessions] = []
dynamoDBObjectMapper.scan(Sessions.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
} else if let paginatedOutput = task.result {
for session in paginatedOutput.items as! [Sessions] {
print("Item Found")
temp.append(session)
}
DispatchQueue.main.async {
self.data = temp
self.didChange.send(self)
}
}
print(self.data.count)
return true
})
}
}
struct Events: View {
#ObservedObject var sessionsData = SessionsData()
var body: some View {...}
}
Looks like you've over-complicated the code. The PassthroughSubject is unnecessary. Whenever you change a #Published property, it should trigger an update.
class SessionsData: ObservableObject {
#Published var data: [Sessions] = []
init() {
load()
}
func load() {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
var temp : [Sessions] = []
dynamoDBObjectMapper.scan(Sessions.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
} else if let paginatedOutput = task.result {
for session in paginatedOutput.items as! [Sessions] {
print("Item Found")
temp.append(session)
}
DispatchQueue.main.async {
self.data = temp
}
}
print(self.data.count)
return true
})
}
}
I don't have experience with DynamoDB, but here are a few things from SwiftUI / Combine perspective. In ObseravbleObjects have change a significant bit and and are now declared with objectWillChange and then sending newValue in willSet:
class SessionsData: ObservableObject {
public let objectWillChange = PassthroughSubject<[Sessions], Never>()
public private(set) var items: [Sessions] = [] {
willSet {
objectWillChange.send(newValue)
}
}
init() {
self.items = []
}
public func load() {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
var temp: [Sessions] = []
dynamoDBObjectMapper
.scan(Sessions.self,
expression: scanExpression)
.continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
} else if let paginatedOutput = task.result,
let sessions = paginatedOutput.items as? [Sessions] {
temp.append(contentsOf: sessions)
}
DispatchQueue.main.async {
self.items = temp
}
}
return true
})
}
}
For the UI part you have to just call your load() method defined above in .onApear() and everything else should happen magically:
struct Events: View {
#ObservedObject var sessionsData: SessionsData
var body: some View {
List {
ForEach(self.sessionsData.items) { session in
Text(session.name) // or something of that kind
}
} .onAppear(perform: { self.sessionsData.load() })
}
}

Resources