Cannot fetch data from Cloud Firestore in SwiftUI Button view - ios

Why does this not work? There is data in Firestore, but it appears the block doesn't execute:
import SwiftUI
import FirebaseFirestore
struct ContentView: View {
var body: some View {
VStack{
Button(
action: {
print("Getting data...")
let db = Firestore.firestore().collection("menu")
let query = db.order(by: "name", descending: true)
query.getDocuments() { snapshot, err in
guard let snapshot = snapshot else {
return
}
print(snapshot)
for doc in snapshot.documents {
print(doc)
}
}
},
label: { Text("Click Me") }
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

You shouldn't add data access logic to a button's action handler. Instead, extract data access logic into a view model (or even a repository), and then add a subscription on the view model's properties like this:
Hypothetical menu item data model:
struct MenuItem: Identifiable {
var id: String = UUID().uuidString
var title: String
}
View Model:
import Foundation
import FirebaseFirestore
class MenuItemsViewModel: ObservableObject {
#Published var menuItems = [MenuItem]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("menuitems").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.menuItems = documents.map { queryDocumentSnapshot -> MenuItem in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
return MenuItem(id: .init(), title: title)
}
}
}
}
And in your view:
struct MenuItemsListView: View {
#ObservedObject var viewModel = MenuItemsViewModel()
var body: some View {
NavigationView {
List(viewModel.menuItems) { menuItem in
VStack(alignment: .leading) {
Text(menuItem.title)
.font(.headline)
}
}
.navigationBarTitle("Menu")
.onAppear() { // (3)
self.viewModel.fetchData()
}
}
}
}
Data mapping can be further simplified by using Firestore's Codable support:
Here's how you need to update the model:
import FirebaseFirestoreSwift
struct MenuItem: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var title: String
}
And here is the updated fetchData() method:
func fetchData() {
db.collection("menuitems").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.menuItems = documents.compactMap { (queryDocumentSnapshot) -> MenuItem? in
return try? queryDocumentSnapshot.data(as: MenuItem.self)
}
}
}
If this doesn't work, check the log for any error messages (your security rules might be preventing you from reading data, for example).

Related

Return Duplicate record with Realm

I am using Relam to store the data locally and working fine but when I try to add the new record with navigation link it returns the duplicate record as well . Another problem is when I click the record , I am expecting change the navigation but since it got duplicate record , the first record does not work but the second one it work .
Here is the Model .
import SwiftUI
import RealmSwift
struct Task: Identifiable {
var id: String
var title: String
var completed: Bool = false
var completedAt: Date = Date()
init(taskObject: TaskObject) {
self.id = taskObject.id.stringValue
self.title = taskObject.title
self.completed = taskObject.completed
self.completedAt = taskObject.completedAt
}
}
Here is the Persisted Model...
import Foundation
import RealmSwift
class TaskObject: Object {
#Persisted(primaryKey: true) var id: ObjectId
#Persisted var title: String
#Persisted var completed: Bool = false
#Persisted var completedAt: Date = Date()
}
Here is the View Model ..
/
/ 2
final class TaskViewModel: ObservableObject {
// 3
#Published var tasks: [Task] = []
// 4
private var token: NotificationToken?
init() {
setupObserver()
}
deinit {
token?.invalidate()
}
// 5
private func setupObserver() {
do {
let realm = try Realm()
let results = realm.objects(TaskObject.self)
token = results.observe({ [weak self] changes in
// 6
self?.tasks = results.map(Task.init)
.sorted(by: { $0.completedAt > $1.completedAt })
.sorted(by: { !$0.completed && $1.completed })
})
} catch let error {
print(error.localizedDescription)
}
}
// 7
func addTask(title: String) {
let taskObject = TaskObject(value: [
"title": title,
"completed": false
])
do {
let realm = try Realm()
try realm.write {
realm.add(taskObject)
}
} catch let error {
print(error.localizedDescription)
}
}
// 8
func markComplete(id: String, completed: Bool) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
try realm.write {
task?.completed = completed
task?.completedAt = Date()
}
} catch let error {
print(error.localizedDescription)
}
}
func remove(id: String) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
if let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId) {
try realm.write {
realm.delete(task)
}
}
} catch let error {
print(error.localizedDescription)
}
}
func updateTitle(id: String, newTitle: String) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
try realm.write {
task?.title = newTitle
}
} catch let error {
print(error.localizedDescription)
}
}
}
Here is the code for Content view ...
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
AddTaskView()
TaskListView()
}
.navigationTitle("Todo")
.navigationBarTitleDisplayMode(.automatic)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is the code for Add task view ..
import SwiftUI
struct AddTaskView: View {
#State private var taskTitle: String = ""
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
HStack(spacing: 12) {
TextField("Enter New Task..", text: $taskTitle)
Button(action: handleSubmit) {
Image(systemName: "plus")
}
}
.padding(20)
}
private func handleSubmit() {
viewModel.addTask(title: taskTitle)
taskTitle = ""
}
}
Here is the Task list View ..
struct TaskListView: View {
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(viewModel.tasks, id: \.id) { task in
TaskRowView(task: task)
Divider().padding(.leading, 20)
NavigationLink (destination: TaskView(task: task)) {
TaskRowView(task: task)
}.animation(.default)
}
}
}
}
}
Here is the code for Row View ..
struct TaskRowView: View {
let task: Task
// 1
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
HStack(spacing: 12) {
Button(action: {
// 2
viewModel.markComplete(id: task.id, completed: !task.completed)
}) {
Image(systemName: task.completed ? "checkmark.circle.fill" : "circle")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(task.completed ? Color.green : Color.gray)
}
VStack(alignment: .leading, spacing: 8) {
Text(task.title)
.foregroundColor(.black)
if !task.completedAt.formatted().isEmpty {
Text(task.completedAt.formatted())
.foregroundColor(.gray)
.font(.caption)
}
}
Spacer()
}
.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20))
}
}
Here is the screenshot ..
Let's troubleshoot the discrepancies one by one.
According to your code, each row in the list represents a Task. But, there are two models Task and TaskObject (persistable model) for that.
struct Task: Identifiable {
var id: String
var title: String
var completed: Bool = false
var completedAt: Date = Date()
init(taskObject: TaskObject) {
self.id = taskObject.id.stringValue
self.title = taskObject.title
self.completed = taskObject.completed
self.completedAt = taskObject.completedAt
}
}
class TaskObject: Object {
#Persisted(primaryKey: true) var id: ObjectId
#Persisted var title: String
#Persisted var completed: Bool = false
#Persisted var completedAt: Date = Date()
}
Instead of using two models, convert them into one.
class TaskObject: Object, Identifiable {
#Persisted(primaryKey: true) var id: ObjectId
#Persisted var title: String
#Persisted var completed: Bool = false
#Persisted var completedAt: Date = Date()
var idStr: String {
id.stringValue
}
}
Therefore, there's no need for mapping to another object after retrieving it from the database. The updated setupObserver function should be...
private func setupObserver() {
do {
let realm = try Realm()
let results = realm.objects(TaskObject.self)
token = results.observe({ [weak self] changes in
// 6
self?.tasks = results
.sorted(by: { $0.completedAt > $1.completedAt })
.sorted(by: { !$0.completed && $1.completed })
})
} catch let error {
print(error.localizedDescription)
}
}
Let's address your questions now.
When I try to add the new record with navigation link it returns the duplicate record as well
It does not produce duplicate data. Instead, the same data is displayed twice in the view. To correct this, remove one of the two instances of TaskRowView(task: task).
struct TaskListView: View {
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(viewModel.tasks, id: \.id) { task in
TaskRowView(task: task) // first row 📌
Divider().padding(.leading, 20)
NavigationLink (destination: TaskView(task: task)) {
TaskRowView(task: task) // second row 📌
}.animation(.default)
}
}
}
}
}
Next question,
I am expecting change the navigation but since it got duplicate record , the first record does not work but the second one it work.
Again, the second one changes navigation, and the first one does not, because this is exactly what is written in the code.
TaskRowView(task: task) // Why would it change navigation?
Divider().padding(.leading, 20)
NavigationLink (destination: TaskView(task: task)) {
TaskRowView(task: task) // changing navigation
}

Why is the data from firebase Firestore not coming into my view | Swift UI

So I have tried for a couple of hours now and it is just not working. I have my firebase Firestore setup and have a collection called leaderboard, which is supposed to store users with their score. However, when I fetch this data and display it, my screen is blank.
Firebase
LeaderBoardModel
import Foundation
import Firebase
class LeaderBoardModel: ObservableObject{
#Published var users = [LeaderBoardItem]()
func getData(){
let db = Firestore.firestore()
db.collection("leaderboard").getDocuments { snapshot, error in
if error == nil{
if let snapshot = snapshot{
DispatchQueue.main.async {
self.users = snapshot.documents.map { d in
return LeaderBoardItem(id: d.documentID, score: d["score"] as? String ?? "", name: d["name"] as? String ?? "", email: d["email"] as? String ?? "")
}
}
}
}
else{
}
}
}
}
LeaderBoardView
import SwiftUI
struct LeaderBoardView: View {
#ObservedObject var leaderBoardModel = LeaderBoardModel()
var body: some View {
List(leaderBoardModel.users) { item in
Text(item.name)
}
}
}
LeaderBoardItem
import Foundation
struct LeaderBoardItem: Identifiable{
var id: String
var score: String
var name: String
var email: String
}
try this approach, using .onAppear{...}:
struct LeaderBoardView: View {
#StateObject var leaderBoardModel = LeaderBoardModel() // <-- here
var body: some View {
List(leaderBoardModel.users) { item in
Text(item.name)
}.onAppear {
leaderBoardModel.getData() // <-- here
}
}
}

How to setup NavigationLink inside SwiftUI list

I am attempting to set up a SwiftUI weather app. when the user searches for a city name in the textfield then taps the search button, a NavigationLink list item should appear in the list. Then, the user should be able to click the navigation link and re-direct to a detail view. My goal is to have the searched navigation links to populate a list. However, my search cities are not populating in the list, and I'm not sure why. In ContentView, I setup a list with a ForEach function that passes in cityNameList, which is an instance of the WeatherViewModel. My expectation is that Text(city.title) should display as a NavigationLink list item. How should I configure the ContentView or ViewModel to populate the the list with NavigationLink list items? See My code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityWeather, id: \.id) { city in
NavigationLink(destination: DetailView(detail: viewModel)) {
HStack {
Text(city.cityWeather.name)
.font(.system(size: 32))
}
}
}
}
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error)
}
}
task.resume()
}
}
Model
import Foundation
struct WeatherModel: Identifiable, Codable {
var id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DetailView
import SwiftUI
struct DetailView: View {
var detail: WeatherViewModel
var body: some View {
VStack(spacing: 20) {
Text(detail.cityWeather.name)
.font(.system(size: 32))
Text("\(detail.cityWeather.main.temp)")
.font(.system(size: 44))
Text(detail.cityWeather.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherViewModel.init())
}
}
try something like this example code, works well for me:
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
}
}
}
}
Spacer()
}.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name).font(.system(size: 32))
Text("\(detail.main.temp)").font(.system(size: 44))
Text(detail.firstWeatherInfo()).font(.system(size: 24))
}
}
}
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}

How to pass data to a sub view from ContentView in SwiftUI

Forgive me if this doesn't make sense, I am a total beginner at Swift. I am creating a recipe app that pulls data from an API and lists it out in navigation links. When the user clicks on the recipe I want it to move to sub view and display information from the API such as recipe name, image, ingredients, and have a button with a link to the webpage.
I was able to get the data pulled into the list with navigation links. However, now I do not know how to go about setting up the recipe details sub view with all of the information I listed above.
This is where I call the API:
class RecipeService {
func getRecipes(_ completion: #escaping (Result<[Recipe], Error>) -> ()) {
let url = URL(string: "http://www.recipepuppy.com/api")!
URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error {
return completion(.failure(error))
}
guard let data = data else {
return completion(.failure(NSError(domain: "", code: -1, userInfo: nil)))
}
do {
let response = try JSONDecoder().decode(RecipesResponses.self, from: data)
completion(.success(response.results))
} catch {
completion(.failure(error))
}
}.resume()
}
}
This is where I take in the recipe responses:
struct RecipesResponses: Codable {
let title: String
let version: Double
let href: String
let results: [Recipe]
}
struct Recipe: Codable {
let title, href, ingredients, thumbnail: String
var detail: URL {
URL(string: href)!
}
var thumb: URL {
URL(string: thumbnail)!
}
}
This is my recipe ViewModel:
class RecipeViewModel: ObservableObject {
#Published var recipes = [Recipe]()
#Published var isLoading = false
private let service = RecipeService()
init() {
loadData()
}
private func loadData() {
isLoading = true
service.getRecipes{ [weak self] result in
DispatchQueue.main.async {
self?.isLoading = false
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let recipes):
self?.recipes = recipes
}
}
}
}
}
This is my view where I list out the API responses:
struct ListView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
NavigationView {
List(viewModel.recipes, id: \.href) { recipe in
NavigationLink (destination: RecipeDetailView()) {
HStack{
CachedImageView(recipe.thumb)
.mask(Circle())
.frame(width: 80)
VStack(alignment: .leading) {
Text(recipe.title)
.font(.largeTitle)
.foregroundColor(.black)
.padding()
}
}
.buttonStyle(PlainButtonStyle())
}
}
.navigationBarTitle(Text("All Recipes"))
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
This is the view where I would like to list out the recipe details and link to the webpage. This is where I am struggling to be able to pull the API data into:
struct RecipeDetailView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
Text("Detail View")
}
}
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView()
}
}
Images of app
You can change RecipeDetailView to accept a Recipe as a parameter:
struct RecipeDetailView: View {
var recipe : Recipe
var body: some View {
Text(recipe.title)
Link("Webpage", destination: recipe.detail)
//etc
}
}
Then, in your NavigationLink, pass the Recipe to it:
NavigationLink(destination: RecipeDetailView(recipe: recipe)) {
One thing I'd warn you about is force unwrapping the URLs in Recipe using ! -- you should know that if you ever get an invalid/malformed URL, this style of unwrapping will crash the app.
Update, to show you what the preview might look like:
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView(recipe: Recipe(title: "Recipe name", href: "https://google.com", ingredients: "Stuff", thumbnail: "https://linktoimage.com"))
}
}

Getting JSON from a public API to render in a list view in SwiftUI

I'm trying to get the data from a Rest Api to download and render in a list view in SwiftUI.
I think I manage to get the JSON to download and assign to all the relevant Structs correctly but nothing displays in the list view on the simulator when I go to build it.
I'm not even sure I need to have the 'Enum CodingKeys in there.
Can anyone point out where I may be going wrong?
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all, I try to find which error of your code and recognize the launch.details of response data somewhere have null. So that I just mark this property Optional and it work.
For more details, you can refer below code :
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
At catch line, get the error message to know exactly what's happen
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
Hope this help!
Any messages in console? I think you need to add NSAppTransportSecurity>NSAllowsArbitraryLoads>YES in your .plist
Transport security has blocked a cleartext HTTP
With the help from Trai Nguyen, I managed to get the simulator to display the data.
All I was missing was to ensure the variable named 'details' had an optional property (by adding on the "?") - like so:
public var details: String?
and then making sure when I render the text in my view, I had to give it a value to insert if the return is null - like so:
Text(launch.details ?? "No Data Found")
Here is the complete code that works for me:
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details ?? "No Data Found")
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
}
/// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources