0

I'm a beginner iOS developer (this is actually my first attempt at an iOS app). The app I'm making is supposed to display pollution levels on a map. What I would like is to set a slider and display the pollution levels for a certain time based on the slider setting.

My initial, apparently wrong approach, was to change the fetchResultsController fetch request predicate, whenever the slider changed and change the displayed results inside of func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>). The problem with this approach is that changing the fetch request doesn't seem to call the controllerDidChangeContent function. It seems that this is only triggered when adding new elements to the db. What is the canonical solution for making this sort of dynamically changing fetch request?

I present below the classes which do the work described above.

CoreDataViewController

import UIKit
import CoreData

class CoreDataViewController: UIViewController, NSFetchedResultsControllerDelegate {
    // MARK: Properties

    var fetchedResultsController : NSFetchedResultsController<NSFetchRequestResult>? {
        didSet {
            // Whenever the frc changes, execute the search and
            // reload the data
            print("fetchedResultsController did set")
            fetchedResultsController?.delegate = self
            executeSearch()
        }
    }

    // MARK: Initializers

    init?(_ coder: NSCoder, fetchedResultsController fc : NSFetchedResultsController<NSFetchRequestResult>) {
        fetchedResultsController = fc
        super.init(coder: coder)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

extension CoreDataViewController {

    func executeSearch() {
        if let fc = fetchedResultsController {
            do {
                try fc.performFetch()
            } catch let e as NSError {
                print("Error while trying to perform a search: \n\(e)\n\(fetchedResultsController)")
            }
        }
    }
}

MapViewController (extends CoreDataViewController)

import UIKit
import GoogleMaps
import CoreData

class MapViewController: CoreDataViewController {

//    var mapView: GMSMapView!
    @IBOutlet weak var testButton: UIBarButtonItem!
    @IBOutlet weak var timestampSlider: UISlider!

    override func loadView() {

        // Get the stack
        let delegate = UIApplication.shared.delegate as! AppDelegate
        let stack = delegate.stack

        // Create a fetchrequest
        let fr = NSFetchRequest<NSFetchRequestResult>(entityName: "Measurement")
        fr.sortDescriptors = [NSSortDescriptor(key: "fromDatetime", ascending: true)]
        fr.predicate = NSPredicate(format: "fromDatetime = %@", argumentArray: [2])

        // Create the FetchedResultController
        fetchedResultsController = NSFetchedResultsController(fetchRequest: fr, managedObjectContext: stack.context,
                                                              sectionNameKeyPath: nil, cacheName: nil)

        let measurements = fetchedResultsController?.fetchedObjects as! [Measurement]

        // Create a GMSCameraPosition that tells the map to display the
        // coordinate latitude: 50.0646501, longitude: 19.9449799 at zoom level 6.
        let camera = GMSCameraPosition.camera(withLatitude: 50.0646501, longitude: 19.9449799, zoom: 14.0)
        let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
        view = mapView

        _ = measurements.flatMap({(m: Measurement) -> Void in
            print(m.fromDatetime)
            let marker = GMSMarker()
            marker.position = CLLocationCoordinate2D(latitude: m.latitude, longitude: m.longitude)
            marker.title = "Measurement"
            marker.snippet = String(format: "pm10: %f, pm25: %f", m.pm10, m.pm25)
            marker.map = mapView
        })

    }

    override func viewWillAppear(_ animated: Bool) {
        self.navigationController?.isToolbarHidden = true

    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    @IBAction func testButtonAction(_ sender: Any) {
        // Get the stack
        let delegate = UIApplication.shared.delegate as! AppDelegate
        let stack = delegate.stack
        _ = Measurement(sensorId: 0, fromDatetime: 2, pm10: 0.6, pm25: 0.3, airQualityIndex: 0.8, latitude: 50.0626402, longitude: 19.9385001, source: "Airly", windDeg: 0.9, windSpeed: 23.0, insertInto: stack.context)
    }

    @IBAction func timestampSliderAction(_ sender: Any) {
        if self.timestampSlider == nil {
            return
        }
        let sliderValue = floor(self.timestampSlider.value)
        print(sliderValue)

        let delegate = UIApplication.shared.delegate as! AppDelegate
        let stack = delegate.stack
        let fr = NSFetchRequest<NSFetchRequestResult>(entityName: "Measurement")
        fr.sortDescriptors = [NSSortDescriptor(key: "fromDatetime", ascending: true)]
        fr.predicate = NSPredicate(format: "fromDatetime = %@", argumentArray: [sliderValue])
//         Create the FetchedResultController
        fetchedResultsController = NSFetchedResultsController(fetchRequest: fr, managedObjectContext: stack.context,
                                                              sectionNameKeyPath: nil, cacheName: nil)

        // alternative solution without creating new NSFetchedResultsController
//        fetchedResultsController?.fetchRequest.predicate = NSPredicate(format: "fromDatetime = %@", argumentArray: [sliderValue])
//        executeSearch()
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        print("controllerDidChangeContent called")
        let measurements = fetchedResultsController?.fetchedObjects as! [Measurement]
        let mapView = view as! GMSMapView
        _ = measurements.flatMap({(m: Measurement) -> Void in
            let marker = GMSMarker()
            marker.position = CLLocationCoordinate2D(latitude: m.latitude, longitude: m.longitude)
            marker.title = "Measurement"
            marker.snippet = String(format: "pm10: %f, pm25: %f", m.pm10, m.pm25)
            marker.map = mapView
        })
    }
}
Lukasz
  • 2,257
  • 3
  • 26
  • 44

1 Answers1

0

The solution turned out to be very simple. To trigger controllerDidChangeContent I simply call fetchedResultsController?.managedObjectContext.refreshAllObjects() after the predicate change. I'm not sure if this is the official way to go, but it worked for me.

Lukasz
  • 2,257
  • 3
  • 26
  • 44