Learn Courses My Dashboard

Module 4 Final Challeng

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.