Skip to content

Latest commit

 

History

History
336 lines (255 loc) · 12 KB

2-NSFetchedResultsController.md

File metadata and controls

336 lines (255 loc) · 12 KB

Getting started with NSFetchResultsController

Simple example of how to use in a UIViewController with a CoreData Entity called Game.

drawing

Create variable.

var fetchedResultsController: NSFetchedResultsController<Game>!

Load via CoreData. Here you can specify how you would like your results to be fetched, give a sort description, and limit the number of results coming back while also setting yourself up as the delegate to receive callbacks. This is also where we load the tableView data.

    func loadSavedData() {
        if fetchedResultsController == nil {
            let request = NSFetchRequest<Game>(entityName: "Game")
            let sort = NSSortDescriptor(key: "name", ascending: false)
            request.sortDescriptors = [sort]
            request.fetchBatchSize = 20

            fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil)
            fetchedResultsController.delegate = self
        }

        do {
            try fetchedResultsController.performFetch()
            tableView.reloadData()
        } catch {
            print("Fetch failed")
        }
    }

When add button is pressed, we can create a new Game Entitiy via CoreData.

    @objc
    func addButtonPressed() {
        guard let name = textField.text else { return }

        // 4 CoreData viewContext > NSFetchRequest > Delegate (us)
        GameManager.shared.createGame(name: name)
	 }

In the UIDataSource we get the data for our tableCell from the fetchResultsController.

extension DemoFetchedResultsViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)

        // 5
        // cell.textLabel?.text = games[indexPath.row]
        let game = fetchedResultsController.object(at: indexPath)
        cell.textLabel?.text = game.name

        cell.accessoryType = UITableViewCell.AccessoryType.none
        
        return cell
    }

Then we can add our trailingSwipeActions only with their respective handlers in the event of a delete. Note here how we fetch the object to delete first from the fetchResultsController and then pass it to our CoreData viewContext to delete.

    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

        let action = UIContextualAction(style: .destructive, title: "Delete", handler: { (action, view, completionHandler) in            
            // 6a. Delete CoreData here
            let game = self.fetchedResultsController.object(at: indexPath)
            GameManager.shared.persistentContainer.viewContext.delete(game)
            GameManager.shared.saveContext()
        })
        action.image = makeSymbolImage(systemName: "trash")

        let configuration = UISwipeActionsConfiguration(actions: [action])

        return configuration
    }

Then update the table only after the fetchResultsController calls us back. This is a three step process: willChangeContent, didChange, didChangeContent.

extension DemoFetchedResultsViewController: NSFetchedResultsControllerDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return fetchedResultsController.sections?.count ?? 0
    }

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates() // a
    }
          
    // 6b Update table via delegate callback here.
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            tableView.insertRows(at: [newIndexPath!], with: .fade) // b
        case .delete:
            tableView.deleteRows(at: [indexPath!], with: .fade)
        case .update:
            tableView.reloadRows(at: [indexPath!], with: .fade)
        case .move:
            tableView.moveRow(at: indexPath!, to: newIndexPath!)
        default:
            break
        }
    }
     
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates() // c
    }
}

Full source.

//
//  DemoFetchedResultsViewController.swift
//  DemoArcade
//
//  Created by Jonathan Rasmusson Work Pro on 2020-03-16.
//  Copyright © 2020 Rasmusson Software Consulting. All rights reserved.
//

import UIKit
import CoreData

class DemoFetchedResultsViewController: UIViewController {

    // Replacing array with CoreData
//    var games = ["Space Invaders",
//                "Dragon Slayer",
//                "Disks of Tron",
//                "Moon Patrol",
//                "Galaga"]

    // 1
    var fetchedResultsController: NSFetchedResultsController<Game>!
    
    let viewContext = GameManager.shared.persistentContainer.viewContext
    
    let textField: UITextField = {
        let textField = UITextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.font = UIFont.preferredFont(forTextStyle: .body)
        textField.textAlignment = .center
        textField.backgroundColor = .systemFill

        return textField
    }()

    lazy var addButton: UIButton = {
        let button = makeButton(withText: "Add")
        button.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
        button.addTarget(self, action: #selector(addButtonPressed), for: .primaryActionTriggered)

        return button
    }()

    var tableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false

        return tableView
    }()

    let cellId = "insertCellId"

    override func viewDidLoad() {
        super.viewDidLoad()
        layout()
        loadSavedData()
        setupTableView()
    }

    func setupTableView() {
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
    }

    func layout() {
        navigationItem.title = "Fetched Results Demo"
        
        let addStackView = makeHorizontalStackView()
        addStackView.addArrangedSubview(textField)
        addStackView.addArrangedSubview(addButton)

        view.addSubview(addStackView)
        view.addSubview(tableView)

        addStackView.topAnchor.constraint(equalToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 3).isActive = true
        addStackView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 3).isActive = true
        view.trailingAnchor.constraint(equalToSystemSpacingAfter: addStackView.trailingAnchor, multiplier: 3).isActive = true

        tableView.topAnchor.constraint(equalToSystemSpacingBelow: addStackView.bottomAnchor, multiplier: 1).isActive = true
        tableView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 1).isActive = true
        view.trailingAnchor.constraint(equalToSystemSpacingAfter: tableView.trailingAnchor, multiplier: 1).isActive = true
        view.safeAreaLayoutGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: tableView.bottomAnchor, multiplier: 1).isActive = true
    }

    // 3
    func loadSavedData() {
        if fetchedResultsController == nil {
            let request = NSFetchRequest<Game>(entityName: "Game")
            let sort = NSSortDescriptor(key: "name", ascending: false)
            request.sortDescriptors = [sort]
            request.fetchBatchSize = 20

            fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil)
            fetchedResultsController.delegate = self
        }

        do {
            try fetchedResultsController.performFetch()
            tableView.reloadData()
        } catch {
            print("Fetch failed")
        }
    }

    // MARK: - Actions

    @objc
    func addButtonPressed() {
        guard let name = textField.text else { return }

        // 4 CoreData viewContext > NSFetchRequest > Delegate (us)
        GameManager.shared.createGame(name: name)

//        games.append(text)
//
//        let indexPath = IndexPath(row: games.count - 1, section: 0)
//
//        tableView.beginUpdates()
//        tableView.insertRows(at: [indexPath], with: .fade)
//        tableView.endUpdates()
    }
}

// MARK:  - UITableView DataSource

extension DemoFetchedResultsViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)

        // 5
        // cell.textLabel?.text = games[indexPath.row]
        let game = fetchedResultsController.object(at: indexPath)
        cell.textLabel?.text = game.name

        cell.accessoryType = UITableViewCell.AccessoryType.none
        
        return cell
    }

    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

        let action = UIContextualAction(style: .destructive, title: "Delete", handler: { (action, view, completionHandler) in
            // 6 Deletion is now a two step process.
//            self.games.remove(at: indexPath.row)
//            tableView.deleteRows(at: [indexPath], with: .fade)
            
            // 6a. Delete CoreData here
            let game = self.fetchedResultsController.object(at: indexPath)
            GameManager.shared.persistentContainer.viewContext.delete(game)
            GameManager.shared.saveContext()
        })
        action.image = makeSymbolImage(systemName: "trash")

        let configuration = UISwipeActionsConfiguration(actions: [action])

        return configuration
    }

    // 7
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sectionInfo = fetchedResultsController.sections![section]
        return sectionInfo.numberOfObjects
    }

}

// 8
extension DemoFetchedResultsViewController: NSFetchedResultsControllerDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return fetchedResultsController.sections?.count ?? 0
    }

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates() // a
    }
          
    // 6b Update table via delegate callback here.
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            tableView.insertRows(at: [newIndexPath!], with: .fade) // b
        case .delete:
            tableView.deleteRows(at: [indexPath!], with: .fade)
        case .update:
            tableView.reloadRows(at: [indexPath!], with: .fade)
        case .move:
            tableView.moveRow(at: indexPath!, to: newIndexPath!)
        default:
            break
        }
    }
     
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates() // c
    }
}

Links that help

Video