Search code examples
swiftsortingcore-datansfetchedresultscontroller

How can I sort a UITableView by a value that's calculated when a UITableViewCell is created?


I have a UITableView in my app that shows a long list of NSManagedObjects of type LiftEvent that are in an NSFetchedResultsController. One of the values displayed, however, isn't a stored property, it's calculated by calling a method on the object when the view model for the UITableViewCell is being created (in the extension):

class LiftLogTableViewCell: UITableViewCell {

    @IBOutlet weak var liftName: UILabel!
    @IBOutlet weak var liftDetails: UILabel!
    @IBOutlet weak var liftDate: UILabel!
    @IBOutlet weak var oneRepMaxWeight: UILabel!
    @IBOutlet weak var unit: UILabel!
    @IBOutlet weak var formula: UILabel!

    var liftWeight: String!
    var repetitions: String!

    struct ViewData {
        let liftName: String
        let liftWeight: Double
        let repetitions: Int
        let liftDate: String
        let oneRepMaxWeight: Double
        let unit: String
        let formula: String
    }

    var viewData: ViewData! {
        didSet {
            liftName!.text = viewData.liftName
            liftWeight = String(viewData.liftWeight)
            repetitions = String(viewData.repetitions)

            let formatter = StringFormatter()
            let formattedWeightLifted = formatter.formatForDisplay(viewData.liftWeight)
            liftDetails!.text = "\(formattedWeightLifted) @ \(viewData.repetitions)"
            liftDate!.text = "on \(viewData.liftDate)"
            oneRepMaxWeight!.text = "\(viewData.oneRepMaxWeight)"
            unit!.text = viewData.unit
            formula!.text = viewData.formula
        }
    }
}

extension LiftLogTableViewCell.ViewData {
    init(liftEvent: LiftEvent) {
        self.liftName = liftEvent.lift.liftName

        // liftEvent.calculatOneRepMax() uses two stored properties of the LiftEvent to calculate the value
        let oneRepMax = liftEvent.calculateOneRepMax() 

        if liftEvent.liftWeight.unit.symbol == UserDefaults.weightUnit() {
            self.liftWeight = liftEvent.liftWeight.value
            self.oneRepMaxWeight = oneRepMax.value
            self.unit = oneRepMax.unit.symbol
                } else {
                    let convertedLiftWeight = ConvertWeightService().convertToOtherUnit(weight: liftEvent.liftWeight)
                    self.liftWeight = convertedLiftWeight.value
                    let convertedMaxWeight = ConvertWeightService().convertToOtherUnit(weight: oneRepMax)
                    self.oneRepMaxWeight = convertedMaxWeight.value
                    self.unit = convertedMaxWeight.unit.symbol
        }

        self.repetitions = Int(liftEvent.repetitions)
        self.formula = liftEvent.formula.formulaName
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let formattedDate = dateFormatter.string(from: liftEvent.date as Date)
        self.liftDate = "\(formattedDate)"

    }
}

I've done my homework found and a thread from two years ago and another thread from over four years ago that say it can't be done when using an NSFecthedResultsController, but Swift is changing so rapidly that I wonder if it might be possible now. I've read Apple's documentation and I couldn't see a way to do it but I'm pretty new to this API.

Is there a way to do this with an NSFetchedResultsController or do I have to give that up and use a simple array?


Solution

  • There is nothing magical about swift that will allow you to create a fetchedResultsController sorted by properties not stored in core data. There are three possible solutions:

    1. Sort by a different property that has the same order. For example you can sort by date and then display by day even though day is not a property it is a derived property from date. You can even use day as a sectionNameKeyPath.
    2. Store the value as a regular property. If it is derived from other properties then you have to update it whenever the other properties are updated.
    3. Do an in-memory sort after you do the fetch.

    1 is the best solution, but I don't understand your model to know if it is possible. If it is not possible I would recommend 2. In general it is a good practice to update your model from only one place so adding a code to sync your derived properties should not be so onerous.