Search code examples
jsoncore-dataswiftui

Mark Core Data entries as Favourites SWIFTUI


I am working on an app that stores posts from on-line Json to CoreData for offline content. It is a Real Estate Listings app where users sell and rent properties.

So far i accomplish to save fethed listings in CoreData but I can not implement "Add to Favourites" for the existing entries. I use NSPredicate to filter the listings types .in different views

  1. I have created a entity "Listing" Core Data - Listing

  2. And a "Favorite" entity Core Data - Favorites

My question is: How can i achieve "Add to Favourite"

Option A: Save the "is favourite" listing one more time in another Entity.

Option B: Create in the "Listing" Entity another "is favourite" property and keep it in Core Data as long as is favourite?

// Here is my Listing Model.swift

struct ListingModel:  Hashable, Decodable, Encodable, Identifiable {
    var id : Int
    var title : String
    var category : String
    var name : String
    var image : String
    var publishdate : String
    var saleprice : Int = 0
    var rentprice : Int = 0
    var listingtype : String
    var latitude : Double!
    var longitude : Double!
    
}
//  JSONViewModel.swift


import SwiftUI
import CoreData

class JSONViewModel: ObservableObject {

    @Published var listings: [ListingModel] = []
  //  @Published var tags :[Tags] = []
    // saving Json to Core Data...
    
    func saveData(contex: NSManagedObjectContext) {
        
        listings.forEach { (data) in
            
            let entity = Listing(context: contex)
            entity.title = data.title
            entity.name = data.name
            entity.category = data.category
            entity.image = data.image
            entity.publishdate = data.publishdate
            entity.tagline = data.tagline
            entity.content = data.content
            entity.coverimage = data.coverimage
            entity.saleprice = Int64(truncating: NSNumber(value: data.saleprice))
            entity.rentprice = Int64(truncating: NSNumber(value: data.rentprice))
            entity.phone = data.phone
            entity.email = data.email
            entity.area = Int64(truncating: NSNumber(value: data.area))
            entity.rooms = Int64(truncating: NSNumber(value: data.rooms))
            entity.beds = Int64(truncating: NSNumber(value: data.beds))
            entity.bathrooms = Int64(truncating: NSNumber(value: data.bathrooms))
            entity.expires = data.expires
            entity.adresa = data.adresa
            entity.listingtype = data.listingtype
            entity.latitude = Double(truncating: NSNumber(value: data.latitude))
            entity.longitude = Double(truncating: NSNumber(value: data.longitude))

        }
        
//        }
        // saving all pending data at once
        
        do{
            try contex.save()
           
            print("success")
        }
        catch{
            print(error.localizedDescription)
        }
    }
    
    func fetchData(context: NSManagedObjectContext){
        
        let url = "https://... my api adress"
        
        var request = URLRequest(url: URL(string: url)!)
        request.addValue("swiftui2.0", forHTTPHeaderField: "field")
        
        let session = URLSession(configuration: .default)
        
        session.dataTask(with: request) { (data, res, _) in
            
            guard let jsonData = data else{return}
            
            // check for errors
            
            let response = res as! HTTPURLResponse
            // checking by status code
            
            if response.statusCode == 404 {
                print("error Api Errror")
            }
            
            // fetching JSON Data ..
            do {
                let listings = try JSONDecoder().decode([ListingModel].self, from: jsonData)
                
                DispatchQueue.main.async {
                  
                    self.listings = listings
                    
                    self.saveData(contex: context)
                }
            }
            catch {
                print(error.localizedDescription)
            }
        }
        .resume()
    }

  // try to extend the function 

func addListingToFavourites(favouritelisting:ListingModel) {
    addRecordToFavourites(favouritelisting:favouritelisting.title)
}
func isListingFavourite(favouritelisting:ListingModel) -> Bool {
    if let _ = fetchRecord(favouritelisting:favouritelisting.title) {
        return true
    }
    return false
}

func removeListingFromFavourites(favouritelisting:ListingModel) {
    removeRecordFromFavourites(favouritelisting:favouritelisting.title)
}

func toggleFavourite(favouritelisting:ListingModel) {
    if isListingFavourite(favouritelisting: favouritelisting) {
        removeListingFromFavourites(favouritelisting: favouritelisting)
    }
    else {
        addListingToFavourites(favouritelisting: favouritelisting)
    }
}
}

and i created also extension JSONViewModel:

extension JSONViewModel {
    
    private func addRecordToFavourites(favouritelisting:String) {
        guard let context = managedContext else {return}
        if let record = fetchRecord(favouritelisting:favouritelisting) {
            print("record \(record) already exists")
            return
        }
        
        let entity = NSEntityDescription.entity(forEntityName: "Favourite",
                                                in: context)!
        let favourite = NSManagedObject(entity:entity, insertInto:context)
        favourite.setValue(favouritelisting, forKeyPath:"favouritelisting")
        
        do {
          try context.save()
        }
        catch let error as NSError {
          print("Could not save. \(error), \(error.userInfo)")
        }
        self.changed = true
    }
    
    private func fetchRecord(favouritelisting:String) -> Favourite? {
        guard let context = managedContext else {return nil}
        
        let request = NSFetchRequest<Favourite>(entityName: "Favourite")
        request.predicate = NSPredicate(format: "favouritelisting == %@", favouritelisting)
        
        if let users = try? context.fetch(request) {
            if users.count > 0 {
                return users[0]
            }
        }
        return nil
    }
    private func removeRecordFromFavourites(favouritelisting:String) {
        guard let context = managedContext else {return}
        if let record = fetchRecord(favouritelisting:favouritelisting) {
            context.delete(record)
            self.changed = true
        }
    }
}

::: Am i on the wright way? still i don't know it this what i should do!

Please find bellow Latest listingsView

import SwiftUI

struct latestListings: View {
    @StateObject var jsonModel = JSONViewModel()
    @Environment(\.managedObjectContext) var context

    @FetchRequest(entity: Listing.entity(),
    sortDescriptors:
    [NSSortDescriptor(keyPath: \Listing.publishdate, ascending: false)])
    
    
    
    var results : FetchedResults<Listing>
    
    var textHeight: CGFloat = 60
    var fullWidth: CGFloat = UIScreen.main.bounds.width
    var cardWidthHalf: CGFloat = UIScreen.main.bounds.width / 2 + UIScreen.main.bounds.width / 3
    var spacing: CGFloat = 10
    var viewHeight: CGFloat = UIScreen.main.bounds.height / 2
    
    @State private var isError = false
    
    var body: some View {
        
        
        VStack(alignment: .leading, spacing: 20) {
            VStack(alignment: .leading, spacing: 10) {
            HStack {
               
                    HStack {
                        Text("Latest Listings")
                            .modifier(textSectionTitle())
                        Spacer()
                        NavigationLink (destination: AllListingsVertical()) {
                            ViewMoreButton()
                        }.buttonStyle(PlainButtonStyle())
                    }
                    .padding(.trailing, 20)
            }
            Divider()
                .modifier(dividerStyle())
               
            HStack {
                Image(systemName: "wand.and.stars.inverse")
                    .modifier(textSectionIcon())
                Text("Manualy download listings to device storage. Useful for offline use.")
                    .modifier(textSectionTagline())
            }
            }
        .padding(.top, 10)
        .padding(.leading, 20)
        .padding(.bottom, 0)
            
            
            ScrollView(.horizontal, showsIndicators: false, content: {
                
                
                
                HStack {
                    // checkin if core data exists
                    if results.isEmpty{
                        
                        if jsonModel.listings.isEmpty{
                            
                            HStack(alignment: .center) {
                                HStack(spacing: 10) {
                                    ProgressView()
                        
                                        .progressViewStyle(CircularProgressViewStyle(tint: Color("dinamicPillsGrass")))
                                        .scaleEffect(2, anchor: .center)
                                        // fetching data
                                        .onAppear(perform: {
                                            jsonModel.fetchData(context: context)
                                            
                                    })
                                }.frame(width: UIScreen.main.bounds.width)
                            }.modifier(cardHeight())
                            
                            // when array is clear indicator appears
                            // as result data is fetched again
                        }
                        else{
                            
                            HStack(spacing: 20) {
                                
                                
                                ForEach(jsonModel.listings,id: \.self){listing in
                                    NavigationLink (destination: CardDetailView(listing: listing)) {
                                        
                                        HStack {
                                            CardViewOrizontal(listing: listing)
                                        }
                                    }.buttonStyle(PlainButtonStyle())
                                    // display fetched Json Data..
                                }
                            }
                        }
                        
                    }
                    
                    else{
                        // results.prefix(?) unde ? cata articole sa arate
                       HStack(spacing: 20) {
                            ForEach(results.prefix(10)){listing in
                                NavigationLink (destination: CardDetailView(fetchedData: listing)) {
                                    
                                    HStack {
                                        
                                        CardViewOrizontal(fetchedData: listing)
                                    }
                                }.buttonStyle(PlainButtonStyle())
                            }
                        }
                        .padding(.trailing, 15)
                        .padding(.leading, 15)
                        
                        
                        
                        
                    }
                    // update finish
                    
                }.padding(.top, 10)
                .padding(.bottom, 10)
                   
                
            })
            
            VStack(alignment: .center) {
                
                Button(action: {

                    // clearing data in core data..

                    if Reachability.isConnectedToNetwork() {
                      //
                        do{
                            jsonModel.listings.removeAll()
                            results.forEach { (listing) in context.delete(listing) }
                            try context.save()
                        }
                        catch{
                            print(error.localizedDescription)
                        }
                        print("Network is connected")
                        self.isError = false
                       
                        
                    } else {

                        print("Network is not connected")
                        self.isError = true
                    }
                    
                    

                }, label: {
                    HStack(alignment: .center) {
                        Image(systemName: "icloud.and.arrow.down")
                            .modifier(textSectionIcon())
                        Text("Update")
                            .modifier(textSectionTagline())
                    }
                    .padding(5)
                    .padding(.trailing, 5)
                    .background(Color("blueLeading"))
                    .cornerRadius(20)
                    .modifier(shadowPills())
                }).alert(isPresented: $isError) {
                    Alert(title: Text("Network is not connected"),
                          message: Text("WiFi or Cellular not availible. You can still browse offline content!"),
                          dismissButton: .default(Text("OK")))
                }
            
            
            }.frame(width: fullWidth)
        }
        .padding(.top, 10)
        .padding(.bottom, 60)
        .frame(width: fullWidth)
        .background(LinearGradient(gradient: Gradient(colors: [Color("dinamicGray1"), Color("dinamicGray2")]), startPoint: .top, endPoint: .bottom))
        .cornerRadius(20)
        .padding(.top, -50)
        .modifier(shadowSection())
    }
}

And CardDetailView

//
//  CardDetailView.swift
//  WebyCoreData
//
//  Created by Marius Geageac on 20.12.2020.
//

import SwiftUI
import KingfisherSwiftUI
import MapKit




struct CardDetailView: View {
  // noul liked
    var fullWidth: CGFloat = UIScreen.main.bounds.width
    var halfScreenH: CGFloat =  UIScreen.main.bounds.height / 2
    
    @ObservedObject var settingsVM = SettingsViewModel()
    
    @State private var isVisible = false

    var listing: ListingModel?
    var fetchedData: Listing?
    // Modifiers
    var cardWidth: CGFloat = UIScreen.main.bounds.width
    var imageWidth: CGFloat = UIScreen.main.bounds.width
    /// map
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: listing == nil ? fetchedData!.latitude : listing!.latitude,
            longitude: listing == nil ? fetchedData!.longitude : listing!.longitude)
    }
   
    let paddingPills: CGFloat = 5
    let textSizePills: CGFloat = 14
    
    var body: some View {
        
        
        ScrollView {
        VStack(alignment: .leading, spacing: 5) {
                        HStack {
                            Text(listing == nil ? fetchedData!.title! : listing!.title)
                                .modifier(textSectionTitle())
                        }
                        Divider()
                            .modifier(dividerStyle())
                        
                        HStack {
                            Image(systemName: "info.circle")
                                .modifier(textSectionIcon())
                            Text(listing == nil ? fetchedData!.tagline! : listing!.tagline)
                                .modifier(textSectionTagline())
                        }
                    }
                    .padding(.top, 10)
                    .padding(.leading, 20)
                    .padding(.bottom, 0)
                    if self.fetchedData!.isFavorite == false {
                    Button(action: {
                        fetchedData!.isFavorite.toggle()
                      }) {
                        Image(systemName: "heart.circle")
                            .modifier(textSectionIcon())
                    }.padding()
                    }
                    else {
                        Button(action: {
                            fetchedData!.isFavorite.toggle()
                          }) {
                            Image(systemName: "heart.circle.fill")
                                .modifier(textSectionIcon())
                        }.padding()
                    }
        
            }
                
        
        
    }
}

import SwiftUI

struct Favorites: View {

@StateObject var jsonModel = JSONViewModel()

var cardWidth: CGFloat = UIScreen.main.bounds.width - 30
var fullWidth: CGFloat = UIScreen.main.bounds.width

// @StateObject var jsonModel = JSONViewModel() @Environment(.managedObjectContext) var context

// Fetching Data From Core Data..
@FetchRequest(entity: Listing.entity(), sortDescriptors:

// [NSSortDescriptor(keyPath: \Listing.publishdate, ascending: false)]) [NSSortDescriptor(keyPath: \Listing.publishdate, ascending: false),],predicate: NSPredicate(format: "isFavourite == %@" , NSNumber(value: true)))

var results : FetchedResults<Listing>



var body: some View {

    
    ScrollView(.vertical) {

        VStack(alignment: .center) {
        VStack(alignment: .center) {

            LazyVStack(spacing: 20) {
                    ForEach(results){listing in
                        NavigationLink (destination: CardDetailView(fetchedData: listing)) {
                            VStack {
                                CardView(fetchedData: listing)
                            }.frame(width: UIScreen.main.bounds.width)
                             .modifier(cardHeight())
                        }.buttonStyle(PlainButtonStyle())
                    }
                }

        }
       
    }.padding(.top, 20)
    }
    .navigationBarTitleDisplayMode(.inline)
    .toolbar {
        ToolbarItem(placement: .principal) {
           
            allListingsTitlePill() // Title
        
        }
        ToolbarItem(placement: .navigationBarTrailing){
            HStack {
                Button(action: {
                }, label: {
                    Image(systemName: "heart.circle.fill")
                        .font(.system(size: 40, weight: .regular))
                })
            }
        }
    }
    
}

}


Solution

  • I would personally just add another property in your listings entity called isFavourite and set it to a boolean. set it initially to false.

    Then when you are doing your fetch requests, you can only show favourites using a predicate like this.

    let predicateIsFavourite = NSPredicate(format: "isFavourite == %@", NSNumber(value: true))

    and in lists / ForEach, you display whether it is a favourite and use a button to toggle it being a favourite. The toggle would just set the value of isFavourite to true (let me know if you would like some code for that but looking at your question, it seems like you know how to do that)