I have a question about my code. In my code I am unable to save the rating of each book in my picker. I am also unable to save if the book is a favourite. Why is this? I have looked in the code and it is not clear to me why the methods are needed. Shouldn’t the state properties be able to save the user input without the .OnChange modifier. Below is my code.
struct BookDetails: View {
@EnvironmentObject var model:BookViewModel
@ObservedObject var book:Book
@State var rating = 0
@State var favourite = 0
@State var img = "star"
var body: some View {
NavigationView{
VStack{
Text(book.title)
.font(.title2)
.fontWeight(.bold)
.padding(.trailing, 200.0)
VStack{
Text("Read Now!")
NavigationLink(destination: {BookPagesView(book: book)}, label: { Image(book.image!)
.resizable()
.scaledToFit()
.frame(width:800, height: 300, alignment: .center)
})
}
.padding(.top,30)
Text("Mark for later!")
.padding()
.bold()
.font(.callout)
Button(action: {
print ("Something")
if favourite == 2 {
img = "star"
favourite = 0
}
else{
img = "star.fill"
favourite = 2
}
}, label: {
Image(systemName: img)
})
.foregroundColor(Color.yellow)
.padding(.bottom, 50.0)
Text("Rate \(book.title)")
.font(.callout)
.bold()
Picker("Rate Amazing Words", selection: $rating, content: {
Text("1").tag(1)
Text("2").tag(2)
Text("3").tag(3)
Text("4").tag(4)
Text("5").tag(5)
})
.pickerStyle(SegmentedPickerStyle())
.padding()
.padding(.bottom,50)
}
}
}
}
@DUDEY_RUDEY
Hi Sean,
Can you post the code you have for your ViewModel and also the code that you have for your data Model?
This is the ViewModel
import Foundation
class BookViewModel:ObservableObject{
@Published var books = [Book]()
init(){
//Get the books array.
self.books = DataService.getLocalData()
}
func changeRating(id:Int, rating:Int){
if let index = books.firstIndex(where: { $0.id == id }){
books[index].rating = rating
}
}
}
This is the data model.
struct DataService{
static func getLocalData() -> [Book]{
let pathString = Bundle.main.path(forResource: "Data", ofType: "json")
guard pathString != nil else {
return [Book]()
}
let url = URL(filePath: pathString!)
let decoder = JSONDecoder()
do {
let data = try Data(contentsOf: url)
do{
let bookData = try decoder.decode([Book].self, from: data)
for index in 0...bookData.count - 1{
bookData[index].image = "cover\(String(index+1))"
//Will append cover1, cover2 ect.
}
return bookData
}
catch{
print(error)
print("Issue in json parsing. Decoding the json to book.")
}
}
catch{
print("error with making data object")
print(error)
}
return [Book]()
}
}
Hey Sean,
The data model I was referring to is what describes an instance of your book and whether it is a class or a struct.
It should have these properties
var id: Int
var title: String
var author: String
var isFavourite: Bool
var currentPage: Int
var rating: Int
var content: [String]
Oops.
My data model does have those properties.
Here it is.
class Book:Decodable,ObservableObject,Identifiable{
var title:String
var author:String
var isFavourite:Bool
var currentPage:Int
var rating:Int
var id:Int
var content:[String]
var image:String?
}
Change your Book
model to a struct and see the difference it makes.
This didn’t make a difference. Regardless I have reworked the code so that I retrieve the images similar to how the solution does. It now looks like this.
Where I get my data
struct DataService{
static func getLocalData() -> [Book]{
let pathString = Bundle.main.path(forResource: "Data", ofType: "json")
guard pathString != nil else {
return [Book]()
}
let url = URL(filePath: pathString!)
let decoder = JSONDecoder()
do {
let data = try Data(contentsOf: url)
do{
let bookData = try decoder.decode([Book].self, from: data)
return bookData
}
catch{
print(error)
print("Issue in json parsing. Decoding the json to book.")
}
}
catch{
print("error with making data object")
print(error)
}
return [Book]()
}
}
My Book Model
struct Book:Decodable,Identifiable{
var title:String
var author:String
var isFavourite:Bool
var currentPage:Int
var rating:Int
var id:Int
var content:[String]
}
The BookDetails View which powers the view in the image at the start of this post.
struct BookDetails: View {
@EnvironmentObject var model:BookViewModel
var book:Book
@State var rating = 0
@State var favourite = 0
@State var img = "star"
var body: some View {
NavigationView{
VStack{
Text(book.title)
.font(.title2)
.fontWeight(.bold)
.padding(.trailing, 140.0)
.frame(width:800)
VStack{
Text("Read Now!")
NavigationLink(destination: {BookPagesView(book: book)}, label: { Image("cover\(book.id)")
.resizable()
.scaledToFit()
.frame(width:800, height: 300, alignment: .center)
})
}
.padding(.top,30)
Text("Mark for later!")
.padding()
.bold()
.font(.callout)
Button(action: {
model.isFavourite(id: book.id)
if favourite == 2 {
img = "star"
favourite = 0
}
else{
img = "star.fill"
favourite = 2
}
}, label: {
Image(systemName: img)
})
.foregroundColor(Color.yellow)
.padding(.bottom, 50.0)
Text("Rate \(book.title)")
.font(.callout)
.bold()
Picker("Rate Amazing Words", selection: $rating, content: {
Text("1").tag(1)
Text("2").tag(2)
Text("3").tag(3)
Text("4").tag(4)
Text("5").tag(5)
})
.pickerStyle(SegmentedPickerStyle())
.padding()
.padding(.bottom,50)
.onChange(of: rating, perform: {value in
model.changeRating(id: book.id, rating: rating)
})
}
}
}
}
@DUDEY_RUDEY
When you make a change to the rating and go back to the ListView (showing all of your books) does the rating change on that View for that book?
Also when you go from the List view to the BookDetails View, you need an .onAppear
to update the @State rating
value (which is bound to the Picker) with the book.rating
value.
Initially when I changed the rating and went back to the ListView than back to Staging the new rating was not saved. I have modified the code. Now with the onAppear modifier the rating value is changing and being saved. I suppose a new instance of the Staging view is being created each time you enter that view from the listView so you need the onAppear modifier to set the rating for that new Staging view. This is my new code with the onAppear modifier.
struct Staging: View {
@EnvironmentObject var model: ViewModel
@State private var rating = 2
var book: Book
var body: some View {
VStack(spacing: 20) {
NavigationLink(destination: BookContent(book: book)) {
VStack {
Text("Read Now!")
.font(.title)
.accentColor(.black)
Image("cover\(book.id)")
.resizable()
.scaledToFit() }
}
.padding()
Text("Mark for later!")
.font(.headline)
Button(action: { model.updateFavourite(forId: book.id) }) {
Image(systemName: book.isFavourite ? "star.fill" : "star")
.resizable()
.frame(width: 28, height: 28)
}
.accentColor(.yellow)
Spacer()
Text("Rate \(book.title)")
.font(.headline)
Picker("Rate this book!", selection: $rating) {
ForEach(1..<6) { index in
Text("\(index)")
.tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding([.leading, .trailing, .bottom], 60)
.onChange(of: rating, perform: { value in
model.updateRating(forId: book.id, rating: rating)
})
}
.onAppear { rating = book.rating }
.navigationBarTitle("\(book.title)")
}
}
When you navigate from the List view to the Staging view, the Staging View is a new instance, as you say, every time that it is called. When you navigate back to the List View the Staging view is destroyed and removed from memory.
The book.rating and @State rating are decoupled from each other so the reason that you were not seeing the rating reflected in the Picker is that it needs to be updated with the current rating value when the Staging view is instantiated, hence the need to have the .onAppear
modifier.
When you select the new rating it is being saved through the .onChange()
modifier.
Also I just realised that in my previous post I was suggesting the rating is reflected in the List View which is just not the case. If you favourite the book then THAT should be reflected in the List view. A little bit of a senior moment there getting things mixed up.