Swiftui #binding ForEach loop with timer function not working - swift

Hy folks, I work on a litte project for a time tracker and use Core Data for storing the values. Every timer Note has a seconds value stored that runs from inside of each timer view. I want to populate now these values to the parent view, but it's not working even when binding the note to the view. I know i have to populate the changes somehow... Can somebody help?
ContentView:
import SwiftUI
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
let coreDM: CoreDataManager
#State private var noteTitle: String = ""
#State private var notes: [Note] = [Note]() // That's the Core Data Model
private func populateNotes() {
notes = coreDM.getAllNotes()
}
var body: some View {
VStack {
if notes.count > 0 {
ForEach(0..<$notes.count,id: \.self) { i in
Text("\(notes[i].seconds)")
}
}
TextField("Enter title", text: $noteTitle)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Save") {
coreDM.saveNote(title: noteTitle, seconds: 0)
populateNotes()
}
List {
if notes.count > 0 {
ForEach(0..<$notes.count,id: \.self) { i in
NoteListView(note: $notes[i], coreDM: coreDM)
Button("Delete"){
coreDM.deleteNote(note: notes[i])
populateNotes()
}
}
}
}.listStyle(PlainListStyle())
Spacer()
}.padding()
.onAppear(perform: {
populateNotes()
})
}
}
NoteListView:
import SwiftUI
import Combine
struct NoteListView: View {
#Binding var note: Note
let coreDM: CoreDataManager
#State private var noteSeconds: Double = 0.0
#State private var noteIsRunning: Bool = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
Text(note.title ?? "")
Text("\(noteSeconds)")
.onReceive(timer) { time in
if noteIsRunning {
noteSeconds += 1
note.seconds = noteSeconds
}
}
if noteIsRunning {
Image(systemName: "pause.circle")
.resizable()
.frame(width:20, height: 20)
.onTapGesture {
withAnimation{
noteIsRunning.toggle()
note.seconds = noteSeconds
coreDM.updateNote()
}
}
}else{
Image(systemName: "record.circle")
.resizable()
.foregroundColor(.orange)
.frame(width:20, height: 20)
.onTapGesture {
withAnimation{
noteIsRunning.toggle()
}
}
}
}
.onAppear(){
noteSeconds = note.seconds
}
}
}
CoreDataManager:
import Foundation
import CoreData
class CoreDataManager {
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name: "TimeTrackerDataModel2")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
}
}
func updateNote() {
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
}
}
func deleteNote(note: Note) {
persistentContainer.viewContext.delete(note)
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
print("Failed to save context \(error)")
}
}
func getAllNotes() -> [Note] {
let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
do {
return try persistentContainer.viewContext.fetch(fetchRequest)
} catch {
return []
}
}
func saveNote(title: String, seconds: Double) {
let note = Note(context: persistentContainer.viewContext)
note.title = title
note.seconds = seconds
do {
try persistentContainer.viewContext.save()
} catch {
print("Failed to save note: \(error)")
}
}
}

Related

SwiftUI: #State value doesn't update after async network request

My aim is the change data in DetailView(). Normally in this case I'm using #State + #Binding and it's works fine with static data, but when I trying to update ViewModel with data from network request I'm loosing functionality of #State (new data doesn't passing to #State value it's stays empty). I checked network request and decoding process - everything ok with it. Sorry my code example a bit long but it's the shortest way that I found to recreate the problem...
Models:
struct LeagueResponse: Decodable {
var status: Bool?
var data: [League] = []
}
struct League: Codable, Identifiable {
let id: String
let name: String
var seasons: [Season]?
}
struct SeasonResponse: Codable {
var status: Bool?
var data: LeagueData?
}
struct LeagueData: Codable {
let name: String?
let desc: String
let abbreviation: String?
let seasons: [Season]
}
struct Season: Codable {
let year: Int
let displayName: String
}
ViewModel:
class LeagueViewModel: ObservableObject {
#Published var leagues: [League] = []
init() {
Task {
try await getLeagueData()
}
}
private func getLeagueData() async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues")!)
guard let leagues = try? JSONDecoder().decode(LeagueResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
self.leagues = leagues.data
}
}
func loadSeasons(forLeague id: String) async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues/\(id)/seasons")!)
guard let seasons = try? JSONDecoder().decode(SeasonResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
print(unwrappedSeasons) // successfully getting and parsing data
}
}
}
}
Views:
struct ContentView: View {
#StateObject var vm = LeagueViewModel()
var body: some View {
NavigationView {
VStack {
if vm.leagues.isEmpty {
ProgressView()
} else {
List {
ForEach(vm.leagues) { league in
NavigationLink(destination: DetailView(league: league)) {
Text(league.name)
}
}
}
}
}
.navigationBarTitle(Text("Leagues"), displayMode: .large)
}
.environmentObject(vm)
}
}
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
if let unwrappedSeasons = league.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.onAppear {
Task {
try await vm.loadSeasons(forLeague: league.id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedLeague: $league)
}
}
}
}
struct ChangeButton: View {
#EnvironmentObject var vm: LeagueViewModel
#Binding var selectedLeague: League // if remove #State the data will pass fine
var body: some View {
Menu {
ForEach(vm.leagues) { league in
Button {
self.selectedLeague = league
} label: {
Text(league.name)
}
}
} label: {
Image(systemName: "calendar")
}
}
}
Main goals:
Show selected league seasons data in DetailView()
Possibility to change seasons data in DetailView() when another league was chosen in ChangeButton()
You update view model but DetailView contains a copy of league (because it is value type).
The simplest seems to me is to return in callback seasons, so there is possibility to update local league as well.
func loadSeasons(forLeague id: String, completion: (([Season]) -> Void)?) async throws {
// ...
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
completion?(unwrappedSeasons) // << here !!
}
}
}
and make task dependent on league id so selection would work, like:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
// ...
}
.task(id: league.id) { // << here !!
Task {
try await vm.loadSeasons(forLeague: league.id) {
league.seasons = $0 // << update local copy !!
}
}
}
Tested with Xcode 13.4 / iOS 15.5
Test module is here
One question is if you already made LeagueViewModel an ObservableObject, why don't you display from it directly, and simply pass an id to your DetailView?
So your detail view will be:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var id: String
var body: some View {
VStack {
if let unwrappedSeasons = vm.leagues.first { $0.id == id }?.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.task(id: id) {
Task {
try await vm.loadSeasons(forLeague: id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedId: $id)
}
}
}
}
The view will automatically update season data as it loads them.

Cannot convert value of type 'Binding<[ContactEntity]>.Element' (aka 'Binding<ContactEntity>') to expected argument type 'ContactEntity'

Using Xcode 13.4.1 on macOS 12.5. I revised the working code to conform to MVVM. This was successful for the first Entity (all properties Optional) for all CRUD operations.
Using this code as a base, I tackled the second Entity (one Bool property NOT Optional), but it throws the compiler error inside the ForEach loop, against 'contact'. This code was error-free before the MVVM conversion. I've been at this for 4 days and am reaching out, but clearly my limited knowledge is inadequate.
ContactListView code below, supported by the ContactViewModel, which in turn relies on the CoreDataManager code.
import SwiftUI
import CoreData
//class FirstNameSort: ObservableObject {
// #Published var firstNameSort: Bool = false
//}
struct ContactsListView: View {
// MARK: - PROPERTIES
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var contactVM = ContactViewModel()
#State private var totalContacts: Int = 0
#State private var search: String = ""
#State private var searchByChampions = false
#State var searchByFirstNames = false
#State private var totalChampions = 0
// MARK: - BODY
var body: some View {
NavigationView {
VStack {
// HStack {
// Toggle("Display Champions only", isOn: $searchByChampions)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
// Toggle("Sort by First Names", isOn: $contactVM.sortFirstName)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
//}
List {
HStack {
Text(searchByChampions ? "Total Champions" : "Total Contacts")
.foregroundColor(.gray)
Spacer()
Text("\(searchByChampions ? totalChampions : totalContacts)")
.bold()
}.foregroundColor(.green)
.padding()
ForEach($contactVM.listofContacts) { contact in
NavigationLink(destination:
ModifyContactView(contact: ***contact***)
.id(UUID()), label: {
ContactRowView(contact: ***contact***)
.id(UUID())
})
}
.onDelete(perform: contactVM.deleteContact)
}.navigationTitle("Contacts")
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: AddContactView(), label: {
Image(systemName: "plus.circle")
})
}
}
.onAppear {
countContacts()
countChampions()
}
.searchable(text: $search, prompt: Text("Contact Last Name?"))
// .onChange(of: search) { value in
// if !value.isEmpty {
// listofContacts.nsPredicate = NSPredicate(format: "contactLastName CONTAINS[dc] %#", value)
// } else {
// listofContacts.nsPredicate = nil
// }
// }
}
}.navigationViewStyle(.stack)
}
func countContacts() {
totalContacts = $contactVM.listofContacts.count
}
// func countChampions() {
// totalChampions = $contactVM.listOfChampions.count
// }
}
import CoreData
import SwiftUI
class ContactViewModel: ObservableObject {
#Environment(\.dismiss) var dismiss
#ObservedObject var dataVM = CoreDataManager()
#ObservedObject var qualifierVM = QualifierViewModel()
#Published var inputFirstName: String = ""
#Published var inputLastName: String = ""
#Published var inputCellNumber: String = ""
#Published var inputEmail: String = ""
#Published var inputChampion: Bool = false
#Published var inputComments: String = ""
#Published var inputCreated: Date = Date()
#Published var inputUpdated: Date = Date()
#Published var listOfFirstNames = []
#Published var listofContacts: [ContactEntity] = []
func fetchContacts() {
let request = NSFetchRequest<ContactEntity>(entityName: "ContactEntity")
do {
dataVM.listofContacts = try dataVM.container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addContact(
contactFirstName: String,
contactLastName: String,
contactCellNumber: String,
contactEmail: String,
contactChampion: Bool,
contactComments: String,
contactCreated: Date,
contactUpdated: Date) {
let newContact = ContactEntity(context: dataVM.container.viewContext)
newContact.contactFirstName = contactFirstName
newContact.contactLastName = contactLastName
newContact.contactCellNumber = contactCellNumber
newContact.contactEmail = contactEmail
newContact.contactChampion = contactChampion
newContact.contactComments = contactComments
newContact.contactUpdated = Date()
newContact.contactCreated = Date()
let uniqueClient = Set(dataVM.selectedClient)
for client in uniqueClient {
newContact.addToClients(client)
print("Client: \(client.clientName ?? "No client")")
}
saveContact()
dismiss()
}
func deleteContact(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = dataVM.listofContacts[index]
dataVM.container.viewContext.delete(entity)
saveContact()
}
func saveContact() {
do {
try dataVM.container.viewContext.save()
fetchContacts()
} catch let error {
print("Error saving. \(error)")
}
}
func sortLastName() -> [ Array<Any>] {
let listOfLastNames = dataVM.listofContacts.sorted {
$0.contactLastName ?? "" < $1.contactLastName ?? ""
}
return [listOfLastNames]
}
func sortFirstName() -> [ Array<Any>] {
let listOfFirstNames = dataVM.listofContacts.sorted {
$0.contactFirstName ?? "" < $1.contactFirstName ?? ""
}
return [listOfFirstNames]
}
}
import Foundation
import CoreData
class CoreDataManager: ObservableObject {
let container: NSPersistentContainer
#Published var listOfQualifiers: [QQEntity] = []
#Published var listofContacts: [ContactEntity] = []
#Published var listOfClients: [ClientEntity] = []
#Published var listOfOpportunities: [OpportunityEntity] = []
//#Published var selectedClient: [ClientEntity] = []
init() {
container = NSPersistentContainer(name: "B2BContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error loading Core Data. \(error)")
} else {
print("Successfully loaded Core Data...")
}
}
}
}

List not re-loading from firestore in base view on dismiss of .sheet in SwiftUI

I have another question.
I have a demo app where I add a ToDo in a Firestore database. From the base View I open a .sheet with a TextEditor where I enter data and save it into Firestore database. But on dismiss the List of ToDos in the base View is gone and is not refreshed until I go to another tab in the app and return back.
I have a ViewModel where I use a Firebase snapshot listener.
Code of the base View:
import Firebase
import Foundation
import SwiftUI
import FirebaseStorage
struct HomeMenuView: View {
#ObservedObject var toDosViewModel = ToDosViewModel()
#Binding var showAddToDoView: Bool
#State private var showModifyToDoView = false
#State private var note = ""
#State private var selectedToDoId = ""
func removeRow(at offset:IndexSet) {
for index in offset {
toDosViewModel.deleteNote(noteToDelete: toDosViewModel.todos[index].id!)
}
}
var body: some View {
ZStack{
VStack (alignment: .center){
List() {
ForEach(toDosViewModel.todos) { todo in
VStack(alignment: .leading, spacing: 10) {
Text(todo.notes)
.font(.subheadline)
.foregroundColor(Color.tabBarColor)
.lineLimit(2)
.onTapGesture {
showAddToDoView = true
selectedToDoId = todo.id!
note = todo.notes
}
}
.listRowSeparatorTint(Color.tabBarColor)
}
.onDelete(perform: removeRow)
}
.listStyle(InsetGroupedListStyle())
.onAppear() {
toDosViewModel.subscribe()
}
}
}
.sheet(isPresented: $showAddToDoView) {
VStack() {
HStack () {
Button("Save") {
guard !note.isEmpty else
{ showAddToDoView = false; return }
toDosViewModel.addNote(notes: note)
note = ""
showAddToDoView = false
}
.offset(x: 20)
Spacer()
Button("Back") {
note = ""
showAddToDoView = false
}
.offset(x: -20)
}
.frame(height: 50, alignment: .center)
TextEditor(
text: $note
)
}
}
}
}
The ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import UIKit
class ToDosViewModel: ObservableObject {
#Published var todos = [ToDo]()
#Published var errorMessage: String?
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("todos")
.order(by: "timestamp", descending: true)
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self?.todos = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ToDo.self) }
switch result {
case .success(let todo):
if let todo = todo {
self?.errorMessage = nil
return todo
}
else {
self?.errorMessage = "Document doesn't exist."
return nil
}
case .failure(let error):
switch error {
case DecodingError.typeMismatch(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self?.errorMessage = "\(error.localizedDescription): \(key)"
default:
self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
return nil
}
}
}
}
}
func addNote(notes: String) {
db.collection("todos").document().setData(["notes" : notes, "timestamp" : FieldValue.serverTimestamp()])
}
func modifyNote(noteToModify: String, notes: String) {
db.collection("todos").document(noteToModify).setData(["notes" : notes, "timestamp" : FieldValue.serverTimestamp()])
}
func deleteNote(noteToDelete: String) {
db.collection("todos").document(noteToDelete).delete()
}
}
Any idea what the issue could be?
Thanks a lot for your support.
Change
#ObservedObject var toDosViewModel = ToDosViewModel()
To
#StateObject var toDosViewModel = ToDosViewModel()

SwiftUI ToDoList with checkboxes?

I want to write a ToDoList in swiftUI with core data. Everything works so far but I want to have a checkbox next to each item it Signify whether it is completed or not.
I have added a property isChecked:boolean in core data but I don't know how to properly read it from the database. How to use a Toggle() in my case?
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ToDoListItem.getAllToDoListItems())
var items: FetchedResults<ToDoListItem>
#State var text: String = ""
var body: some View {
NavigationView {
List {
Section (header: Text("NewItem")){
HStack {
TextField("Enter new Item.",text: $text)
Button(action: {
if !text.isEmpty{
let newItem = ToDoListItem(context: context)
newItem.name = text
newItem.createdAt = Date()
// current date as created
newItem.isChecked = false
do {
try context.save()
} catch {
print(error)
}
// to clear the textField from the previous entry
text = ""
}
}, label: {
Text("Save")
})
}// end of vstack
}
Section {
ForEach(items){ toDoListItem in
VStack(alignment: .leading){
// to have a checkbox
Button {
toDoListItem.isChecked.toggle()
} label: {
Label(toDoListItem.name!, systemImage: toDoListItem.isChecked ? "checkbox.square" : "square")
}
if let name = toDoListItem.name {
// Toggle(isOn: toDoListItem.isChecked)
Text(name)
.font(.headline)
}
//Text(toDoListItem.name!)
//.font(.headline)
if let createdAt = toDoListItem.createdAt {
//Text("\(toDoListItem.createdAt!)")
Text("\(createdAt)")
}
}
}.onDelete(perform: { indexSet in
guard let index = indexSet.first else {
return
}
let itemToDelete = items[index]
context.delete(itemToDelete)
do {
try context.save()
}
catch {
print(error)
}
})
}
}
.navigationTitle("To Do List")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ToDoListItem.swift
class ToDoListItem: NSManagedObject,Identifiable {
#NSManaged var name:String?
#NSManaged var createdAt:Date?
#NSManaged var isChecked:Bool
// mapped to the entry properties in database
}
extension ToDoListItem {
static func getAllToDoListItems() -> NSFetchRequest<ToDoListItem>{
let request:NSFetchRequest<ToDoListItem> = ToDoListItem.fetchRequest() as!
NSFetchRequest<ToDoListItem>
// cast as todolist item
let sort = NSSortDescriptor(key: "createdAt", ascending: true)
// above order of sorting
request.sortDescriptors = [sort]
return request
}
}
Should isChecked be an optional as well?

ListView in child view is not refreshed correctly

There is a ListView. I make a transaction in Cloud Firestore by changing the field of an element when I click on it in the list. Data in the database changes as it should, but after this action all the elements in the list disappear (although there is .onAppear {fetchData}). An important point: this is a child view, there is no such problem in the parent view.
I also added a button at the bottom of the list to execute fetchData (), when I click on it, the data returns to the list
What could be the problem? Thanks
import SwiftUI
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update data"){
let updBook = book
self.viewModel.myTransaction(book: updBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
Button("update list"){
self.viewModel.fetchData()
}
}
}
}
ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBook(book: Book){
if let bookID = book.id{
db.collection("books").document(bookID).delete()
}
}
func updateBook(book: Book) {
if let bookID = book.id{
do {
try db.collection("books").document(bookID).setData(from: book) }
catch {
print(error)
}
}
}
func addBook(book: Book) {
do {
let _ = try db.collection("books").addDocument(from: book)
}
catch {
print(error)
}
}
func myTransaction(book: Book){
let bookID = book.id
let targetReference = db.collection("books").document(bookID!)
db.runTransaction({ (transaction, errorPointer) -> Any? in
let targetDocument: DocumentSnapshot
do {
try targetDocument = transaction.getDocument(targetReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let oldValue = targetDocument.data()?["pages"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(targetDocument)"
]
)
errorPointer?.pointee = error
return nil
}
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
transaction.updateData(["pages": oldValue + 1], forDocument: targetReference)
return nil
}) { (object, error) in
if let error = error {
print("Transaction failed: \(error)")
} else {
print("Transaction successfully committed!")
}
}
}
}
Parent view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update"){
let delBook = book
self.viewModel.myTransaction(book: delBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
NavigationLink(destination: SecondView()){
Text("Second View")
}
}
}
}
}
A possible solution might be that your Views and its ViewModels interfere with each other. It looks like you create two instances of the same BookViewModel:
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
Try creating one BooksViewModel and pass it between views (you can use an #EnvironmentObject).