Hyoryusha
Hyoryusha

Reputation: 207

SwiftUI and Core Data: Using a fetch request with instance member as argument inside a view

I seem to be caught in a Catch-22 situation. Perhaps my approach is entirely wrong here. I hope someone can help. I want to create a star-based rating display using feedback from users who visit a particular real world landmark. In CoreData I have an entity called Rating with attributes called rating (Int32) and landmark (String). I want to get the average for all rating(s) associated with a given landmark in order to display stars in the view for each. Here is the code for the View:

struct TitleImageView: View {
    @Environment(\.managedObjectContext) var viewContext : NSManagedObjectContext
    let landmark: Landmark 
    var body: some View {
        
        Image(landmark.imageName)
            .resizable()
            .shadow(radius: 10 )
            .border(Color.white)
            .scaledToFit()
            .padding([.leading, .trailing], 40)
            .layoutPriority(1)
            .overlay(TextOverlay(landmark: landmark))
            .overlay(RatingsOverlay(rating: stars))
    }
}

Here is the fetch (which works as expected when the argument for the fetch is hard coded):

let fetchRequest = Rating.fetchRequestForLandmark(landmark: landmark.name)
    var ratings: FetchedResults<Rating> {
            fetchRequest.wrappedValue
    }
        var sum: Int32 {
            ratings.map { $0.rating }.reduce(0, +)
        }
        var stars : Int32 {
            sum / Int32(ratings.count)
        } 

The problem is this: When I insert the fetch before the body of the view, I get the warning

"Cannot use instance member 'landmark' within property initializer; property initializers run before 'self' is available"

When I place the fetch after the body, I get:

"Closure containing a declaration cannot be used with function builder 'ViewBuilder'" (with reference to var ratings)

Is there an easy way out of this conundrum or must I go back to the proverbial drawingboard? Thanks.

Upvotes: 2

Views: 1572

Answers (2)

Hyoryusha
Hyoryusha

Reputation: 207

Thanks to those who offered suggestions. After a long hiatus, I returned to this problem and arrived at this solution (based on a HackingWithSwift tutorial: https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui) Here's the code:

struct RatingsView: View {
    var fetchRequest: FetchRequest<Rating>
    
    var body: some View {

        HStack(spacing: 0.4){
            ForEach(1...5) { number in
                if number > stars {
                    //Image(systemName: "star")
                } else {
                    Image(systemName: "star.fill")
                }
            }
        }
    }
    
    init(filter: String) {
        fetchRequest = FetchRequest<Rating>(entity: Rating.entity() , sortDescriptors: [], predicate: NSPredicate(format: "%K == %@", "landmark" , filter))
    }
    var sum: Int16 {
        fetchRequest.wrappedValue.reduce(0) { $0 + $1.rating }
        }
    
    var stars : Int {
        var starCount:Int
        if fetchRequest.wrappedValue.count > 0 {
            starCount = Int(sum) / fetchRequest.wrappedValue.count
        } else {
            starCount = 0
        }
        return starCount
    }
}

I'm not sure if this is the absolute best solution, but it's working as intended. Hope this can help others.

Upvotes: 0

Helperbug
Helperbug

Reputation: 567

How about wrapping the fetch in a calculated property that returns a sum & stars tuple? Slightly different shape with the same results. Calculated properties can reference other properties, so the swift-init hurdle is cleared!

var metrics : (sum: Int, stars: Int) {
let fetchRequest = Rating.fetchRequestForLandmark(landmark: landmark.name)
var ratings: FetchedResults<Rating> {
    fetchRequest.wrappedValue
}

let sum = ratings.map { $0.rating }.reduce(0, +)
let stars = sum / ratings.count

return (sum: sum, stars: stars)

}

Upvotes: 0

Related Questions