Utilize NSFetchedResultsController to update the deck when answers are recorded.

This commit is contained in:
CypherPoet 2020-01-22 10:06:19 -06:00
parent 7b3cbd9038
commit a1efa0f6c6
8 changed files with 111 additions and 28 deletions

View File

@ -34,7 +34,7 @@
F378AA0E23D4CC6400296A76 /* DraggableCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA0D23D4CC6400296A76 /* DraggableCardView.swift */; };
F378AA1023D5BAD500296A76 /* CardDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA0F23D5BAD500296A76 /* CardDeckView.swift */; };
F378AA1323D63AA700296A76 /* NumberFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1223D63AA700296A76 /* NumberFormatters.swift */; };
F378AA1523D7156200296A76 /* Card+Outcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1423D7156100296A76 /* Card+Outcome.swift */; };
F378AA1523D7156200296A76 /* Card+AnswerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1423D7156100296A76 /* Card+AnswerState.swift */; };
F378AA1723D733EF00296A76 /* EditDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1623D733EE00296A76 /* EditDeckView.swift */; };
F378AA1923D7359B00296A76 /* EditDeckView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1823D7359B00296A76 /* EditDeckView+ViewModel.swift */; };
F378AA1B23D7A06600296A76 /* CountdownTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1A23D7A06500296A76 /* CountdownTimerView.swift */; };
@ -43,6 +43,7 @@
F378AA2323D7B8D900296A76 /* CardDeck+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA2123D7B8D900296A76 /* CardDeck+CoreDataProperties.swift */; };
F378AA2523D7B91E00296A76 /* CardDeck+Computeds.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA2423D7B91E00296A76 /* CardDeck+Computeds.swift */; };
F378AA3023D8909C00296A76 /* KeyboardAvoider in Frameworks */ = {isa = PBXBuildFile; productRef = F378AA2F23D8909C00296A76 /* KeyboardAvoider */; };
F378AA3223D8A6EA00296A76 /* Card+FetchHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA3123D8A6EA00296A76 /* Card+FetchHelpers.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -72,7 +73,7 @@
F378AA0D23D4CC6400296A76 /* DraggableCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableCardView.swift; sourceTree = "<group>"; };
F378AA0F23D5BAD500296A76 /* CardDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDeckView.swift; sourceTree = "<group>"; };
F378AA1223D63AA700296A76 /* NumberFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatters.swift; sourceTree = "<group>"; };
F378AA1423D7156100296A76 /* Card+Outcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Card+Outcome.swift"; sourceTree = "<group>"; };
F378AA1423D7156100296A76 /* Card+AnswerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Card+AnswerState.swift"; sourceTree = "<group>"; };
F378AA1623D733EE00296A76 /* EditDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditDeckView.swift; sourceTree = "<group>"; };
F378AA1823D7359B00296A76 /* EditDeckView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditDeckView+ViewModel.swift"; sourceTree = "<group>"; };
F378AA1A23D7A06500296A76 /* CountdownTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownTimerView.swift; sourceTree = "<group>"; };
@ -80,6 +81,7 @@
F378AA2023D7B8D900296A76 /* CardDeck+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CardDeck+CoreDataClass.swift"; sourceTree = "<group>"; };
F378AA2123D7B8D900296A76 /* CardDeck+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CardDeck+CoreDataProperties.swift"; sourceTree = "<group>"; };
F378AA2423D7B91E00296A76 /* CardDeck+Computeds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CardDeck+Computeds.swift"; sourceTree = "<group>"; };
F378AA3123D8A6EA00296A76 /* Card+FetchHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Card+FetchHelpers.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -216,7 +218,8 @@
isa = PBXGroup;
children = (
F378A9F423D44E3800296A76 /* Card+CoreDataClass.swift */,
F378AA1423D7156100296A76 /* Card+Outcome.swift */,
F378AA3123D8A6EA00296A76 /* Card+FetchHelpers.swift */,
F378AA1423D7156100296A76 /* Card+AnswerState.swift */,
F378A9F523D44E3900296A76 /* Card+CoreDataProperties.swift */,
);
path = Card;
@ -382,6 +385,7 @@
buildActionMask = 2147483647;
files = (
F378AA0423D48D6D00296A76 /* CardDeckContainerView+ViewModel.swift in Sources */,
F378AA3223D8A6EA00296A76 /* Card+FetchHelpers.swift in Sources */,
F378AA1E23D7A0B900296A76 /* CountdownTimerView+ViewModel.swift in Sources */,
F378A9F623D44E3900296A76 /* Card+CoreDataClass.swift in Sources */,
F378AA0C23D4A5D300296A76 /* View+Stacked.swift in Sources */,
@ -400,7 +404,7 @@
F378A9E623D43A6900296A76 /* CoreDataManager+Utils.swift in Sources */,
F378AA1023D5BAD500296A76 /* CardDeckView.swift in Sources */,
F378AA1723D733EF00296A76 /* EditDeckView.swift in Sources */,
F378AA1523D7156200296A76 /* Card+Outcome.swift in Sources */,
F378AA1523D7156200296A76 /* Card+AnswerState.swift in Sources */,
F378AA1923D7359B00296A76 /* EditDeckView+ViewModel.swift in Sources */,
F378A9F323D43CAA00296A76 /* PreviewData+Cards.swift in Sources */,
F378AA0223D48CB900296A76 /* CardDeckContainerView.swift in Sources */,

View File

@ -12,7 +12,7 @@ import CoreData
@objc(CardDeck)
public class CardDeck: NSManagedObject {
}
extension CardDeck: Identifiable {}

View File

@ -21,6 +21,7 @@ extension CardDeck {
@NSManaged public var cards: NSSet?
}
// MARK: Generated accessors for cards
extension CardDeck {

View File

@ -12,11 +12,6 @@ import CoreData
extension Card {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Card> {
return NSFetchRequest<Card>(entityName: "Card")
}
@NSManaged public var prompt: String?
@NSManaged public var answer: String?
@NSManaged public var decks: NSSet?

View File

@ -0,0 +1,51 @@
//
// Card+FetchHelpers.swift
// Flashzilla
//
// Created by CypherPoet on 1/22/20.
//
//
import Foundation
import CoreData
import CypherPoetCoreDataKit_PredicateUtils
extension Card {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Card> {
return NSFetchRequest<Card>(entityName: "Card")
}
public enum SortDescriptors {
}
public enum Predicate {
public static func cards(in cardDeck: CardDeck) -> NSPredicate {
let keyword = NSComparisonPredicate.keyword(for: .contains)
let predicate = NSPredicate(
format: "%K \(keyword) %@",
#keyPath(Card.decks),
cardDeck.objectID
)
return predicate
}
}
@nonobjc public class func fetchRequest(
forCardsIn cardDeck: CardDeck
) -> NSFetchRequest<Card> {
let fetchRequest: NSFetchRequest<Card> = Self.fetchRequest()
fetchRequest.sortDescriptors = []
fetchRequest.predicate = Self.Predicate.cards(in: cardDeck)
return fetchRequest
}
}

View File

@ -16,7 +16,11 @@ public protocol FetchedResultsControlling: NSObject {
var fetchRequest: NSFetchRequest<FetchedResult> { get }
var fetchedResultsController: NSFetchedResultsController<FetchedResult> { get }
func makeFetchedResultsController() -> FetchedResultsController
func makeFetchedResultsController(
sectionNameKeyPath: String?,
cacheName: String?
) -> FetchedResultsController
func extractResults(from fetchedResultsController: FetchedResultsController) -> [FetchedResult]
}
@ -26,12 +30,15 @@ extension FetchedResultsControlling {
public typealias FetchRequest = NSFetchRequest<FetchedResult>
public func makeFetchedResultsController() -> FetchedResultsController {
public func makeFetchedResultsController(
sectionNameKeyPath: String? = nil,
cacheName: String? = nil
) -> FetchedResultsController {
.init(
fetchRequest: fetchRequest,
managedObjectContext: CurrentApp.coreDataManager.mainContext,
sectionNameKeyPath: nil,
cacheName: nil
sectionNameKeyPath: sectionNameKeyPath,
cacheName: cacheName
)
}

View File

@ -10,13 +10,20 @@
import SwiftUI
import Combine
import CoreData
import CypherPoetCoreDataKit
extension CardDeckContainerView {
final class ViewModel: ObservableObject {
final class ViewModel: NSObject, ObservableObject, FetchedResultsControlling {
typealias FetchedResult = Card
lazy var fetchRequest: NSFetchRequest<Card> = Card.fetchRequest(forCardsIn: cardDeck)
internal lazy var fetchedResultsController: FetchedResultsController = makeFetchedResultsController()
private var subscriptions = Set<AnyCancellable>()
@ObservedObject var cardDeck: CardDeck
var isTimerActive = false
@ -25,6 +32,7 @@ extension CardDeckContainerView {
// MARK: - Published Outputs
@Published var cards: [Card] = []
@Published var visibleCards: [Card] = []
@Published var timeRemaining: TimeInterval
@ -37,7 +45,11 @@ extension CardDeckContainerView {
self.roundDuration = roundDuration
self.timeRemaining = roundDuration
super.init()
self.fetchedResultsController.delegate = self
setupSubscribers()
fetchCards()
}
}
}
@ -65,22 +77,17 @@ extension CardDeckContainerView.ViewModel {
}
private var cardsPublisher: Publishers.Share<AnyPublisher<[Card], Never>> {
cardDeck.publisher(for: \.cards)
.map { _ in self.cardDeck.cardsArray }
private var visibleCardsPublisher: AnyPublisher<[Card], Never> {
$cards
.map { $0.filter { $0.answerState == .unanswered } }
// .print("visibleCardsPublisher")
.eraseToAnyPublisher()
.share()
}
}
// MARK: - Computeds
extension CardDeckContainerView.ViewModel {
var visibleCards: [Card] {
cards.filter { $0.answerState == .unanswered }
}
var isDeckEmpty: Bool { visibleCards.isEmpty }
}
@ -89,6 +96,12 @@ extension CardDeckContainerView.ViewModel {
// MARK: - Public Methods
extension CardDeckContainerView.ViewModel {
func fetchCards() {
try? fetchedResultsController.performFetch()
cards = extractResults(from: fetchedResultsController)
}
func resetDeck() {
cards.forEach { $0.answerState = .unanswered }
@ -100,13 +113,14 @@ extension CardDeckContainerView.ViewModel {
isTimerActive = false
}
func resumeRound() {
isTimerActive = true
}
func record(_ answerState: Card.AnswerState, forCardAt index: Int) {
let card = cards[index]
let card = visibleCards[index]
guard let managedObjectContext = card.managedObjectContext else { fatalError() }
@ -120,14 +134,13 @@ extension CardDeckContainerView.ViewModel {
}
// MARK: - Private Helpers
private extension CardDeckContainerView.ViewModel {
func setupSubscribers() {
cardsPublisher
visibleCardsPublisher
.receive(on: DispatchQueue.main)
.assign(to: \.cards, on: self)
.assign(to: \.visibleCards, on: self)
.store(in: &subscriptions)
timeRemainingPublisher
@ -148,3 +161,15 @@ private extension CardDeckContainerView.ViewModel {
.store(in: &subscriptions)
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension CardDeckContainerView.ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let controller = controller as? FetchedResultsController else { return }
print("controllerDidChangeContent")
cards = extractResults(from: controller)
}
}