Refactor Core Data model to support adding and removing cards in the edit view.
This commit is contained in:
parent
4cd7256a60
commit
cfe895744c
|
@ -39,6 +39,9 @@
|
|||
F378AA1923D7359B00296A76 /* EditDeckView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1823D7359B00296A76 /* EditDeckView+ViewModel.swift */; };
|
||||
F378AA1B23D7A06600296A76 /* CountdownTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1A23D7A06500296A76 /* CountdownTimerView.swift */; };
|
||||
F378AA1E23D7A0B900296A76 /* CountdownTimerView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA1D23D7A0B900296A76 /* CountdownTimerView+ViewModel.swift */; };
|
||||
F378AA2223D7B8D900296A76 /* CardDeck+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA2023D7B8D900296A76 /* CardDeck+CoreDataClass.swift */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
@ -73,6 +76,9 @@
|
|||
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>"; };
|
||||
F378AA1D23D7A0B900296A76 /* CountdownTimerView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CountdownTimerView+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -188,6 +194,7 @@
|
|||
F378A9DD23D4396000296A76 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F378AA1F23D7B8B900296A76 /* Card Deck */,
|
||||
F378A9DF23D4399000296A76 /* Card */,
|
||||
F378A9C223D438AE00296A76 /* Flashzilla.xcdatamodeld */,
|
||||
);
|
||||
|
@ -278,6 +285,16 @@
|
|||
path = "Countdown Timer";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F378AA1F23D7B8B900296A76 /* Card Deck */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F378AA2023D7B8D900296A76 /* CardDeck+CoreDataClass.swift */,
|
||||
F378AA2423D7B91E00296A76 /* CardDeck+Computeds.swift */,
|
||||
F378AA2123D7B8D900296A76 /* CardDeck+CoreDataProperties.swift */,
|
||||
);
|
||||
path = "Card Deck";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -366,6 +383,7 @@
|
|||
F378AA0C23D4A5D300296A76 /* View+Stacked.swift in Sources */,
|
||||
F378A9F723D44E3900296A76 /* Card+CoreDataProperties.swift in Sources */,
|
||||
F378A9E823D43B6A00296A76 /* CurrentApplication.swift in Sources */,
|
||||
F378AA2323D7B8D900296A76 /* CardDeck+CoreDataProperties.swift in Sources */,
|
||||
F378A9FD23D47E1000296A76 /* CardView+ViewModel.swift in Sources */,
|
||||
F378A9E423D43A1700296A76 /* PreviewData.swift in Sources */,
|
||||
F378A9DA23D4393100296A76 /* RootView.swift in Sources */,
|
||||
|
@ -382,9 +400,11 @@
|
|||
F378AA1923D7359B00296A76 /* EditDeckView+ViewModel.swift in Sources */,
|
||||
F378A9F323D43CAA00296A76 /* PreviewData+Cards.swift in Sources */,
|
||||
F378AA0223D48CB900296A76 /* CardDeckContainerView.swift in Sources */,
|
||||
F378AA2223D7B8D900296A76 /* CardDeck+CoreDataClass.swift in Sources */,
|
||||
F378A9FB23D47DD900296A76 /* CardView.swift in Sources */,
|
||||
F378AA0723D4985200296A76 /* FetchedResultsControlling.swift in Sources */,
|
||||
F378A9C123D438AE00296A76 /* SceneDelegate.swift in Sources */,
|
||||
F378AA2523D7B91E00296A76 /* CardDeck+Computeds.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -32,10 +32,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
|
||||
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
|
||||
// Add `@Environment(\.managedObjectContext)` in the views that will need the context.
|
||||
let contentView = RootView()
|
||||
let entryView = RootView()
|
||||
.environment(\.managedObjectContext, context)
|
||||
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
window.rootViewController = UIHostingController(rootView: entryView)
|
||||
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// CardDeck+Computeds.swift
|
||||
// Flashzilla
|
||||
//
|
||||
// Created by CypherPoet on 1/21/20.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
extension CardDeck {
|
||||
var cardsArray: [Card] {
|
||||
guard let cardSet = cards as? Set<Card> else { return [] }
|
||||
|
||||
return Array(cardSet)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// CardDeck+CoreDataClass.swift
|
||||
// Flashzilla
|
||||
//
|
||||
// Created by Brian Sipple on 1/21/20.
|
||||
// Copyright © 2020 CypherPoet. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(CardDeck)
|
||||
public class CardDeck: NSManagedObject {
|
||||
|
||||
}
|
||||
|
||||
extension CardDeck: Identifiable {}
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// CardDeck+CoreDataProperties.swift
|
||||
// Flashzilla
|
||||
//
|
||||
// Created by Brian Sipple on 1/21/20.
|
||||
// Copyright © 2020 CypherPoet. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension CardDeck {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<CardDeck> {
|
||||
return NSFetchRequest<CardDeck>(entityName: "CardDeck")
|
||||
}
|
||||
|
||||
@NSManaged public var name: String?
|
||||
@NSManaged public var cards: NSSet?
|
||||
}
|
||||
|
||||
// MARK: Generated accessors for cards
|
||||
extension CardDeck {
|
||||
|
||||
@objc(addCardsObject:)
|
||||
@NSManaged public func addToCards(_ value: Card)
|
||||
|
||||
@objc(removeCardsObject:)
|
||||
@NSManaged public func removeFromCards(_ value: Card)
|
||||
|
||||
@objc(addCards:)
|
||||
@NSManaged public func addToCards(_ values: NSSet)
|
||||
|
||||
@objc(removeCards:)
|
||||
@NSManaged public func removeFromCards(_ values: NSSet)
|
||||
|
||||
}
|
|
@ -19,12 +19,31 @@ extension Card {
|
|||
|
||||
@NSManaged public var prompt: String?
|
||||
@NSManaged public var answer: String?
|
||||
@NSManaged public var decks: NSSet?
|
||||
|
||||
@NSManaged public var answerStateValue: Int16
|
||||
|
||||
@NSManaged public var answerStateValue: Int16
|
||||
|
||||
var answerState: Card.AnswerState {
|
||||
get { Card.AnswerState(rawValue: answerStateValue)! }
|
||||
set { answerStateValue = newValue.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Generated accessors for decks
|
||||
extension Card {
|
||||
|
||||
@objc(addDecksObject:)
|
||||
@NSManaged public func addToDecks(_ value: CardDeck)
|
||||
|
||||
@objc(removeDecksObject:)
|
||||
@NSManaged public func removeFromDecks(_ value: CardDeck)
|
||||
|
||||
@objc(addDecks:)
|
||||
@NSManaged public func addToDecks(_ values: NSSet)
|
||||
|
||||
@objc(removeDecks:)
|
||||
@NSManaged public func removeFromDecks(_ values: NSSet)
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<attribute name="answer" optional="YES" attributeType="String"/>
|
||||
<attribute name="answerStateValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="prompt" optional="YES" attributeType="String"/>
|
||||
<relationship name="decks" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="CardDeck" inverseName="cards" inverseEntity="CardDeck"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="answer"/>
|
||||
|
@ -11,7 +12,17 @@
|
|||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="CardDeck" representedClassName="CardDeck" syncable="YES">
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<relationship name="cards" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Card" inverseName="decks" inverseEntity="Card"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="name"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Card" positionX="-63" positionY="-18" width="128" height="88"/>
|
||||
<element name="Card" positionX="-63" positionY="-18" width="128" height="103"/>
|
||||
<element name="CardDeck" positionX="-54" positionY="18" width="128" height="73"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -48,9 +48,23 @@ extension PreviewData {
|
|||
|
||||
static let `default` = card1
|
||||
|
||||
|
||||
static func buildDeck() -> [Card] {
|
||||
[card3, card1, card2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PreviewData {
|
||||
|
||||
enum CardDecks {
|
||||
static let `default`: CardDeck = {
|
||||
let deck = CardDeck(context: CurrentApp.coreDataManager.mainContext)
|
||||
|
||||
deck.name = "Preview Deck"
|
||||
|
||||
for card in [PreviewData.Cards.card3, PreviewData.Cards.card1, PreviewData.Cards.card2] {
|
||||
deck.addToCards(card)
|
||||
}
|
||||
|
||||
return deck
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ import CoreData
|
|||
enum PreviewData {
|
||||
|
||||
static func setupSimulatorPreviewData(in managedObjectContext: NSManagedObjectContext) {
|
||||
let _ = PreviewData.Cards.buildDeck()
|
||||
|
||||
try? managedObjectContext.save()
|
||||
let _ = PreviewData.CardDecks.default
|
||||
let _ = try? CurrentApp.coreDataManager.saveContexts()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// CountdownTimerView.swift
|
||||
// Flashzilla
|
||||
//
|
||||
// Created by CypherPoet on 1/21/20.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct CountdownTimerView {
|
||||
var viewModel: ViewModel
|
||||
}
|
||||
|
||||
|
||||
// MARK: - View
|
||||
extension CountdownTimerView: View {
|
||||
var body: some View {
|
||||
Text("Time Remaining: ")
|
||||
.font(.title)
|
||||
.foregroundColor(Color("Accent3"))
|
||||
|
||||
+ Text(self.viewModel.timeRemainingText)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color("Accent3"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
struct CountdownTimerView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
CountdownTimerView(
|
||||
viewModel: .init()
|
||||
)
|
||||
.environment(\.managedObjectContext, CurrentApp.coreDataManager.mainContext)
|
||||
}
|
||||
}
|
|
@ -14,24 +14,12 @@ import CoreData
|
|||
|
||||
extension CardDeckContainerView {
|
||||
|
||||
final class ViewModel: NSObject, FetchedResultsControlling, ObservableObject {
|
||||
typealias FetchedResult = Card
|
||||
final class ViewModel: ObservableObject {
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
|
||||
internal lazy var fetchRequest: NSFetchRequest<Card> = {
|
||||
let request: NSFetchRequest<Card> = Card.fetchRequest()
|
||||
|
||||
request.sortDescriptors = []
|
||||
|
||||
return request
|
||||
}()
|
||||
|
||||
|
||||
internal lazy var fetchedResultsController: FetchedResultsController = makeFetchedResultsController()
|
||||
|
||||
|
||||
var isTimerActive = true
|
||||
@ObservedObject var cardDeck: CardDeck
|
||||
|
||||
var isTimerActive = false
|
||||
var roundDuration: TimeInterval
|
||||
|
||||
|
||||
|
@ -42,16 +30,14 @@ extension CardDeckContainerView {
|
|||
|
||||
// MARK: - Init
|
||||
init(
|
||||
cardDeck: CardDeck,
|
||||
roundDuration: TimeInterval = 100.0
|
||||
) {
|
||||
self.cardDeck = cardDeck
|
||||
self.roundDuration = roundDuration
|
||||
self.timeRemaining = roundDuration
|
||||
|
||||
super.init()
|
||||
|
||||
self.fetchedResultsController.delegate = self
|
||||
setupSubscribers()
|
||||
fetchCards()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,12 +56,21 @@ extension CardDeckContainerView.ViewModel {
|
|||
|
||||
private var timeRemainingPublisher: AnyPublisher<TimeInterval, Never> {
|
||||
roundTickPublisher
|
||||
.drop(while: { _ in !self.isTimerActive })
|
||||
.map { _ in
|
||||
self.isTimerActive ? max(0, self.timeRemaining - 1.0) : self.timeRemaining
|
||||
}
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
private var cardsPublisher: Publishers.Share<AnyPublisher<[Card], Never>> {
|
||||
cardDeck.publisher(for: \.cards)
|
||||
.map { _ in self.cardDeck.cardsArray }
|
||||
.eraseToAnyPublisher()
|
||||
.share()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -94,12 +89,6 @@ 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 }
|
||||
|
||||
|
@ -114,6 +103,20 @@ extension CardDeckContainerView.ViewModel {
|
|||
func resumeRound() {
|
||||
isTimerActive = true
|
||||
}
|
||||
|
||||
|
||||
func record(_ answerState: Card.AnswerState, forCardAt index: Int) {
|
||||
let card = cards[index]
|
||||
|
||||
guard let managedObjectContext = card.managedObjectContext else { fatalError() }
|
||||
|
||||
card.answerState = answerState
|
||||
CurrentApp.coreDataManager.save(managedObjectContext)
|
||||
|
||||
if isDeckEmpty {
|
||||
pauseRound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -122,6 +125,11 @@ extension CardDeckContainerView.ViewModel {
|
|||
private extension CardDeckContainerView.ViewModel {
|
||||
|
||||
func setupSubscribers() {
|
||||
cardsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.cards, on: self)
|
||||
.store(in: &subscriptions)
|
||||
|
||||
timeRemainingPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.timeRemaining, on: self)
|
||||
|
@ -140,15 +148,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import CypherPoetSwiftUIKit
|
|||
|
||||
|
||||
struct CardDeckContainerView {
|
||||
@ObservedObject var viewModel: ViewModel = .init()
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
|
||||
@State private var isShowingEditView = false
|
||||
}
|
||||
|
@ -24,6 +24,11 @@ extension CardDeckContainerView: View {
|
|||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
VStack(spacing: 32) {
|
||||
|
||||
// 📝: It can be tricky having `CardDeckContainerView` contain the timer, because SwiftUI
|
||||
// will re-render it on every tick.
|
||||
// Perhaps it would be better to have `CountdownTimerView` own its timer
|
||||
// and drive it with `timeRemaining`?
|
||||
CountdownTimerView(
|
||||
viewModel: .init(timeRemaining: self.viewModel.timeRemaining)
|
||||
)
|
||||
|
@ -32,7 +37,9 @@ extension CardDeckContainerView: View {
|
|||
width: min(max(800, geometry.size.width) * 0.8, 480),
|
||||
height: min(max(800, geometry.size.width) * 0.8, 480) * 0.5,
|
||||
cards: self.viewModel.visibleCards,
|
||||
cardAnswered: { (answerState, index) in self.record(answerState, forCardAt: index) }
|
||||
cardAnswered: { (answerState, index) in
|
||||
self.viewModel.record(answerState, forCardAt: index)
|
||||
}
|
||||
)
|
||||
.allowsHitTesting(self.viewModel.timeRemaining > 0)
|
||||
}
|
||||
|
@ -59,9 +66,12 @@ extension CardDeckContainerView: View {
|
|||
.edgesIgnoringSafeArea(.all)
|
||||
.sheet(isPresented: self.$isShowingEditView, onDismiss: self.viewModel.resumeRound) {
|
||||
EditDeckView(
|
||||
viewModel: .init(currentDeck: self.viewModel.cards)
|
||||
viewModel: .init(currentDeck: self.viewModel.cardDeck)
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
self.viewModel.isTimerActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,15 +114,6 @@ private extension CardDeckContainerView {
|
|||
// func countdownFinished() {
|
||||
// viewModel.pauseRound()
|
||||
// }
|
||||
|
||||
|
||||
func record(_ answerState: Card.AnswerState, forCardAt index: Int) {
|
||||
viewModel.cards[index].answerState = answerState
|
||||
|
||||
if viewModel.isDeckEmpty {
|
||||
viewModel.pauseRound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -121,7 +122,9 @@ private extension CardDeckContainerView {
|
|||
struct CardDeckContainerView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
CardDeckContainerView()
|
||||
CardDeckContainerView(
|
||||
viewModel: .init(cardDeck: PreviewData.CardDecks.default)
|
||||
)
|
||||
.environment(\.managedObjectContext, CurrentApp.coreDataManager.mainContext)
|
||||
// .previewLayout(PreviewLayout.iPhone11Landscape)
|
||||
}
|
||||
|
|
|
@ -20,13 +20,13 @@ extension EditDeckView {
|
|||
|
||||
|
||||
// MARK: - Published Outputs
|
||||
@Published var currentDeck: [Card]
|
||||
@Published var currentDeck: CardDeck
|
||||
@Published var canAddNewCard: Bool = false
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
init(
|
||||
currentDeck: [Card] = [],
|
||||
currentDeck: CardDeck,
|
||||
newCard: Card = Card(context: CurrentApp.coreDataManager.backgroundContext)
|
||||
) {
|
||||
print("EditDeckView+ViewModel || init")
|
||||
|
@ -67,7 +67,7 @@ extension EditDeckView.ViewModel {
|
|||
newCardPromptTextPublisher,
|
||||
newCardAnswerTextPublisher
|
||||
)
|
||||
.print("canAddNewCardPublisher")
|
||||
// .print("canAddNewCardPublisher")
|
||||
.map { !$0.0.isEmpty && !$0.1.isEmpty }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ extension EditDeckView.ViewModel {
|
|||
|
||||
// MARK: - Computeds
|
||||
extension EditDeckView.ViewModel {
|
||||
var cards: [Card] { currentDeck.cardsArray }
|
||||
}
|
||||
|
||||
|
||||
|
@ -83,17 +84,23 @@ extension EditDeckView.ViewModel {
|
|||
extension EditDeckView.ViewModel {
|
||||
|
||||
func addNewCard() {
|
||||
currentDeck.append(newCard)
|
||||
// TODO: Persist changes here.
|
||||
guard let managedObjectContext = currentDeck.managedObjectContext else { fatalError() }
|
||||
|
||||
currentDeck.addToCards(newCard)
|
||||
CurrentApp.coreDataManager.save(managedObjectContext)
|
||||
|
||||
newCard = makeNewCard()
|
||||
}
|
||||
|
||||
|
||||
func removeCards(at offsets: IndexSet) {
|
||||
currentDeck.remove(atOffsets: offsets)
|
||||
|
||||
// TODO: Persist changes here.
|
||||
guard let managedObjectContext = currentDeck.managedObjectContext else { fatalError() }
|
||||
|
||||
for offset in offsets {
|
||||
currentDeck.removeFromCards(cards[offset])
|
||||
}
|
||||
|
||||
CurrentApp.coreDataManager.save(managedObjectContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ extension EditDeckView: View {
|
|||
|
||||
|
||||
Section(header: Text("Current Cards")) {
|
||||
ForEach(viewModel.currentDeck) { card in
|
||||
ForEach(viewModel.cards) { card in
|
||||
VStack(alignment: .leading) {
|
||||
Text(card.prompt ?? "")
|
||||
.font(.headline)
|
||||
|
@ -87,7 +87,7 @@ struct EditDeckView_Previews: PreviewProvider {
|
|||
|
||||
EditDeckView(
|
||||
viewModel: .init(
|
||||
currentDeck: [PreviewData.Cards.card1, PreviewData.Cards.card2]
|
||||
currentDeck: PreviewData.CardDecks.default
|
||||
)
|
||||
)
|
||||
.environment(\.managedObjectContext, CurrentApp.coreDataManager.mainContext)
|
||||
|
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
|||
|
||||
|
||||
struct RootView {
|
||||
@FetchRequest(sortDescriptors: [], animation: nil) var cardDecks: FetchedResults<CardDeck>
|
||||
}
|
||||
|
||||
|
||||
|
@ -17,7 +18,11 @@ struct RootView {
|
|||
extension RootView: View {
|
||||
|
||||
var body: some View {
|
||||
CardDeckContainerView()
|
||||
// 📝 In a production app, we'd want to make sure that the user's
|
||||
// decks were properly fetched here.
|
||||
CardDeckContainerView(
|
||||
viewModel: .init(cardDeck: cardDecks.first!)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue