I am new to SwiftUI and trying doing wrap-up challenge.
I do not know why when I pass a list as Environment Object, change the element's attribute of the list to update the UI of the button, the button cannot display the change despite its attribute has been changed successfully.
Here is my project structure: https://i.stack.imgur.com/7KEaK.png
Main file: BookLibraryAppApp
import SwiftUI
@main
struct BookLibraryAppApp: App {
var body: some Scene {
WindowGroup {
BookListView()
.environmentObject(BookModel())
}
}
}
Model file: Book
import Foundation
class Book : Decodable, Identifiable {
var id = 1
var title = "Title"
var author = "Author"
var content = ["I am a test book."]
var isFavourite = false
var rating = 2
var currentPage = 0
}
On view model file: BookModel
import Foundation
class BookModel : ObservableObject {
@Published var books = [Book]()
init() {
books = getLoadData()
}
func getLoadData() -> [Book] {
let fileName = "Data"
let fileExtension = "json"
var books = [Book]()
// Get link to data file
let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension)
guard url != nil else {
print("Could not retrieve category data: \(fileName).\(fileExtension) not found.")
return books
}
do {
// Decode the data and return it
let data = try Data(contentsOf: url!)
books = try JSONDecoder().decode([Book].self, from: data)
return books
} catch {
print("Error retrieving category data: \(error.localizedDescription)")
}
return books
}
func updateFavourite(forId: Int) {
if let index = books.firstIndex(where: { $0.id == forId }) {
books[index].isFavourite.toggle()
}
}
func getBookFavourite(forId: Int) -> Bool {
if let index = books.firstIndex(where: { $0.id == forId }) {
return books[index].isFavourite
}
return false
}
}
On View folder:
- BookListView
import SwiftUI
struct BookListView: View {
@EnvironmentObject var model : BookModel
var body: some View {
NavigationView {
ScrollView {
LazyVStack(spacing: 50) {
ForEach(model.books, id: \.id) {
book in
NavigationLink {
BookPreviewView(book: book)
} label: {
BookCardView(book: book)
}
}
}
.padding(25)
}
.navigationTitle("My Library")
}
}
}
struct BookListView_Previews: PreviewProvider {
static var previews: some View {
BookListView()
.environmentObject(BookModel())
}
}
- On smaller view: BookCardView
import SwiftUI
struct BookCardView: View {
var book : Book
var body: some View {
ZStack {
// MARK: container
Rectangle()
.cornerRadius(20)
.foregroundColor(.white)
.shadow(color: Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 0.5), radius: 10, x: -5, y: 5)
// MARK: card book content
VStack (alignment: .leading, spacing: 9) {
// MARK: title and favourite
HStack {
Text(book.title)
.font(.title2)
.bold()
Spacer()
if (book.isFavourite) {
// Image(systemName: "star.fill")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: 30)
// .foregroundColor(.yellow)
}
}
// MARK: author
Text(book.author)
.font(.subheadline)
.italic()
// MARK: image
Image("cover\(book.id)")
.resizable()
.aspectRatio(contentMode: .fit)
}
.padding(.horizontal, 35)
.padding(.vertical, 30)
}
}
}
struct BookCardView_Previews: PreviewProvider {
static var previews: some View {
BookCardView(book: BookModel().books[0])
}
}
- On smaller view: BookPreviewView
import SwiftUI
struct BookPreviewView: View {
@EnvironmentObject var model : BookModel
// @State var select =
var book : Book
var body: some View {
VStack {
// MARK: banner
Text("Read Now!")
Spacer()
// MARK: cover image
Image("cover\(book.id)")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
Spacer()
// MARK: bookmark
Text("Mark for later")
Button {
// destination
self.model.updateFavourite(forId: book.id)
// self.select.toggle()
print(model.getBookFavourite(forId: book.id))
} label: {
// label
Image(systemName: self.book.isFavourite ? "star.fill" : "star")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 28)
.foregroundColor(.yellow)
}
Spacer()
// MARK: rating
Text("Rate Amazing Words")
Spacer()
}
}
}
struct BookPreviewView_Previews: PreviewProvider {
static var previews: some View {
BookPreviewView(book: BookModel().books[0])
}
}
Here is Data.json file:
[
{
"title": "Amazing Words",
"author": "Sir Prise Party",
"isFavourite": true,
"currentPage": 0,
"rating": 2,
"id": 1,
"content": [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla eu congue lacus",
"ac laoreet felis. Integer eros tortor, blandit id magna non, pharetra sodalesurna."
]},
{
"title": "Text and More",
"author": "Sir Vey Sample",
"isFavourite": false,
"currentPage": 0,
"rating": 2,
"id": 3,
"content": [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla eu congue lacus",
"ac laoreet felis. Integer eros tortor, blandit id magna non, pharetra sodalesurna.
]}
]
When I try to click the Yellow start on the BookPreviewView, instead of changing from "star" to "star.fill", it shows nothing. May I ask what's wrong with my code? Thank you very much!
CodePudding user response:
This is a pretty simple error. The @Published
property wrapper will send a notification to the view to update itself as soon as its value changes.
But in your case this never happens. You defined Book
as a class (reference type), so changing one of its property doesn´t force the array (valuetype) to change, so @Published
doesn´t pick up the change.
Two solutions here:
If you insist on keeping the class use:
func updateFavourite(forId: Int) { if let index = books.firstIndex(where: { $0.id == forId }) { objectWillChange.send() // add this books[index].isFavourite.toggle() } }
this will send the notification by hand.
the prefered solution should be to make your model
Book
a struct and it will pick up the changes.