Complete Challenge 2

> Add a settings screen that has a single option: Whether or not a card goes back into deck if it was answered incorrectly.
This commit is contained in:
CypherPoet 2020-01-23 15:49:26 -06:00
parent 934e328595
commit c9bf108b51
13 changed files with 308 additions and 11 deletions

View File

@ -44,6 +44,12 @@
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 */; };
F378AA3623D91B5100296A76 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA3523D91B5100296A76 /* AppState.swift */; };
F378AA3823D91B8C00296A76 /* SettingsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA3723D91B8C00296A76 /* SettingsState.swift */; };
F378AA3B23D91BD900296A76 /* Burritos in Frameworks */ = {isa = PBXBuildFile; productRef = F378AA3A23D91BD900296A76 /* Burritos */; };
F378AA3E23D91DFE00296A76 /* SettingsFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA3D23D91DFE00296A76 /* SettingsFormView.swift */; };
F378AA4023D91E1500296A76 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA3F23D91E1500296A76 /* SettingsContainerView.swift */; };
F378AA4223D9215400296A76 /* PreviewData+AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F378AA4123D9215400296A76 /* PreviewData+AppStore.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -82,6 +88,11 @@
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>"; };
F378AA3523D91B5100296A76 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
F378AA3723D91B8C00296A76 /* SettingsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsState.swift; sourceTree = "<group>"; };
F378AA3D23D91DFE00296A76 /* SettingsFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFormView.swift; sourceTree = "<group>"; };
F378AA3F23D91E1500296A76 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = "<group>"; };
F378AA4123D9215400296A76 /* PreviewData+AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreviewData+AppStore.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -90,6 +101,7 @@
buildActionMask = 2147483647;
files = (
F378A9EB23D43C3200296A76 /* CypherPoetSwiftUIKit in Frameworks */,
F378AA3B23D91BD900296A76 /* Burritos in Frameworks */,
F378A9EE23D43C5600296A76 /* CypherPoetCoreDataKit in Frameworks */,
F378AA3023D8909C00296A76 /* KeyboardAvoider in Frameworks */,
F378A9F123D43C6F00296A76 /* CypherPoetSwiftUIAnimationKit in Frameworks */,
@ -152,6 +164,7 @@
F378A9D623D438D900296A76 /* Scenes */ = {
isa = PBXGroup;
children = (
F378AA3C23D91D9700296A76 /* Settings */,
F378A9D923D4393100296A76 /* RootView.swift */,
F378AA0023D48CA900296A76 /* Card Deck */,
);
@ -191,6 +204,8 @@
F378A9DC23D4395D00296A76 /* State */ = {
isa = PBXGroup;
children = (
F378AA3523D91B5100296A76 /* AppState.swift */,
F378AA3723D91B8C00296A76 /* SettingsState.swift */,
);
path = State;
sourceTree = "<group>";
@ -210,6 +225,7 @@
children = (
F378A9E323D43A1700296A76 /* PreviewData.swift */,
F378A9F223D43CA900296A76 /* PreviewData+Cards.swift */,
F378AA4123D9215400296A76 /* PreviewData+AppStore.swift */,
);
path = "Preview Data";
sourceTree = "<group>";
@ -300,6 +316,15 @@
path = "Card Deck";
sourceTree = "<group>";
};
F378AA3C23D91D9700296A76 /* Settings */ = {
isa = PBXGroup;
children = (
F378AA3D23D91DFE00296A76 /* SettingsFormView.swift */,
F378AA3F23D91E1500296A76 /* SettingsContainerView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -321,6 +346,7 @@
F378A9ED23D43C5600296A76 /* CypherPoetCoreDataKit */,
F378A9F023D43C6F00296A76 /* CypherPoetSwiftUIAnimationKit */,
F378AA2F23D8909C00296A76 /* KeyboardAvoider */,
F378AA3A23D91BD900296A76 /* Burritos */,
);
productName = Flashzilla;
productReference = F378A9BB23D438AE00296A76 /* Flashzilla.app */;
@ -355,6 +381,7 @@
F378A9EC23D43C5600296A76 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */,
F378A9EF23D43C6E00296A76 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIAnimationKit" */,
F378AA2E23D8909C00296A76 /* XCRemoteSwiftPackageReference "KeyboardAvoider" */,
F378AA3923D91BD900296A76 /* XCRemoteSwiftPackageReference "Burritos" */,
);
productRefGroup = F378A9BC23D438AE00296A76 /* Products */;
projectDirPath = "";
@ -391,6 +418,7 @@
F378AA0C23D4A5D300296A76 /* View+Stacked.swift in Sources */,
F378A9F723D44E3900296A76 /* Card+CoreDataProperties.swift in Sources */,
F378A9E823D43B6A00296A76 /* CurrentApplication.swift in Sources */,
F378AA3823D91B8C00296A76 /* SettingsState.swift in Sources */,
F378AA2323D7B8D900296A76 /* CardDeck+CoreDataProperties.swift in Sources */,
F378A9FD23D47E1000296A76 /* CardView+ViewModel.swift in Sources */,
F378A9E423D43A1700296A76 /* PreviewData.swift in Sources */,
@ -400,11 +428,14 @@
F378AA1323D63AA700296A76 /* NumberFormatters.swift in Sources */,
F378A9C423D438AE00296A76 /* Flashzilla.xcdatamodeld in Sources */,
F378AA0E23D4CC6400296A76 /* DraggableCardView.swift in Sources */,
F378AA3E23D91DFE00296A76 /* SettingsFormView.swift in Sources */,
F378A9BF23D438AE00296A76 /* AppDelegate.swift in Sources */,
F378A9E623D43A6900296A76 /* CoreDataManager+Utils.swift in Sources */,
F378AA1023D5BAD500296A76 /* CardDeckView.swift in Sources */,
F378AA1723D733EF00296A76 /* EditDeckView.swift in Sources */,
F378AA1523D7156200296A76 /* Card+AnswerState.swift in Sources */,
F378AA3623D91B5100296A76 /* AppState.swift in Sources */,
F378AA4023D91E1500296A76 /* SettingsContainerView.swift in Sources */,
F378AA1923D7359B00296A76 /* EditDeckView+ViewModel.swift in Sources */,
F378A9F323D43CAA00296A76 /* PreviewData+Cards.swift in Sources */,
F378AA0223D48CB900296A76 /* CardDeckContainerView.swift in Sources */,
@ -412,6 +443,7 @@
F378A9FB23D47DD900296A76 /* CardView.swift in Sources */,
F378AA0723D4985200296A76 /* FetchedResultsControlling.swift in Sources */,
F378A9C123D438AE00296A76 /* SceneDelegate.swift in Sources */,
F378AA4223D9215400296A76 /* PreviewData+AppStore.swift in Sources */,
F378AA2523D7B91E00296A76 /* CardDeck+Computeds.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -640,6 +672,14 @@
minimumVersion = 1.0.1;
};
};
F378AA3923D91BD900296A76 /* XCRemoteSwiftPackageReference "Burritos" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/guillermomuntaner/Burritos.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -663,6 +703,11 @@
package = F378AA2E23D8909C00296A76 /* XCRemoteSwiftPackageReference "KeyboardAvoider" */;
productName = KeyboardAvoider;
};
F378AA3A23D91BD900296A76 /* Burritos */ = {
isa = XCSwiftPackageProductDependency;
package = F378AA3923D91BD900296A76 /* XCRemoteSwiftPackageReference "Burritos" */;
productName = Burritos;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "Burritos",
"repositoryURL": "https://github.com/guillermomuntaner/Burritos.git",
"state": {
"branch": null,
"revision": "309dbe1b5b3af8839ca6a7ebb2ad6ddf041bf420",
"version": "0.0.3"
}
},
{
"package": "CypherPoetCoreDataKit",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetCoreDataKit.git",

View File

@ -30,10 +30,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
PreviewData.setupSimulatorPreviewData(in: context)
#endif
let store = AppStore(initialState: .init(), appReducer: appReducer)
// 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 entryView = RootView()
.environment(\.managedObjectContext, context)
.environmentObject(store)
window.rootViewController = UIHostingController(rootView: entryView)

View File

@ -0,0 +1,37 @@
//
// AppState.swift
// Flashzilla
//
// Created by CypherPoet on 1/22/20.
//
//
import Foundation
import CypherPoetSwiftUIKit_DataFlowUtils
struct AppState {
var settingsState = SettingsState()
}
//enum AppSideEffect: SideEffect {}
enum AppAction {
case settings(_ action: SettingsAction)
}
// MARK: - Reducer
let appReducer = Reducer<AppState, AppAction> { appState, action in
switch action {
case .settings(let action):
settingsReducer.reduce(&appState.settingsState, action)
}
}
typealias AppStore = Store<AppState, AppAction>

View File

@ -0,0 +1,35 @@
//
// SettingsState.swift
// Flashzilla
//
// Created by CypherPoet on 1/22/20.
//
//
import CypherPoetSwiftUIKit_DataFlowUtils
import UserDefault
import Combine
struct SettingsState {
@UserDefault("settings-state-restacks-incorrect-cards", defaultValue: false)
var restacksIncorrectCards: Bool
}
enum SettingsAction {
case restacksIncorrectCardsSet(Bool)
}
// MARK: - Reducer
let settingsReducer: Reducer<SettingsState, SettingsAction> = Reducer(
reduce: { state, action in
switch action {
case .restacksIncorrectCardsSet(let value):
state.restacksIncorrectCards = value
}
}
)

View File

@ -0,0 +1,20 @@
//
// PreviewData+AppStore.swift
// Flashzilla
//
// Created by CypherPoet on 1/22/20.
//
//
import Foundation
extension PreviewData {
enum AppStores {
static let `default`: AppStore = {
AppStore(initialState: .init(), appReducer: appReducer)
}()
}
}

View File

@ -63,7 +63,8 @@ extension CardView {
private var cardContent: some View {
Group {
if isAccessibilityEnabled {
// if isAccessibilityEnabled { // 📝 This appears to become true whenever a sheet is presented and dismissed over the deck view (Xcode 11.3.1)
if false {
Text(isShowingAnswer ? viewModel.cardAnswerText : viewModel.cardPromptText)
.font(.largeTitle)
} else {

View File

@ -42,11 +42,16 @@ extension DraggableCardView: View {
.clipShape(
RoundedRectangle(cornerRadius: min(geometry.size.width, geometry.size.height) * 0.08)
)
.animation(.easeIn(duration: 0.25))
)
.animation(nil)
.rotationEffect(self.cardRotation)
.offset(self.cardOffset)
// TODO: We'd need to manually animate this back into position if we don't
// want to have to cancel the animations before it. (Look into using @State instead
// of @GestureState -- since the latter automatically handles resettting on end)
.animation(.spring())
.gesture(self.dragGesture)
}

View File

@ -29,7 +29,7 @@ extension CardDeckContainerView {
var isTimerActive = false
var roundDuration: TimeInterval
var settingsState: SettingsState
// MARK: - Published Outputs
@Published var cards: [Card] = []
@ -40,11 +40,13 @@ extension CardDeckContainerView {
// MARK: - Init
init(
cardDeck: CardDeck,
roundDuration: TimeInterval = 100.0
roundDuration: TimeInterval = 100.0,
settingsState: SettingsState = .init()
) {
self.cardDeck = cardDeck
self.roundDuration = roundDuration
self.timeRemaining = roundDuration
self.settingsState = settingsState
super.init()
@ -79,10 +81,21 @@ extension CardDeckContainerView.ViewModel {
private var visibleCardsPublisher: AnyPublisher<[Card], Never> {
$cards
.map { $0.filter { $0.answerState == .unanswered } }
.print("visibleCardsPublisher")
.eraseToAnyPublisher()
Publishers.CombineLatest(
$cards,
CurrentValueSubject(settingsState)
)
.map { (cards, settingsState) in
cards.filter { card in
let answerState = card.answerState
// An optimization here would be to include sorting logic that accounted
// for the answer state. That way, incorrect cards could slide to the back.
return answerState == .unanswered ||
(settingsState.restacksIncorrectCards && answerState == .incorrect)
}
}
.eraseToAnyPublisher()
}
}
@ -109,7 +122,7 @@ extension CardDeckContainerView.ViewModel {
var unansweredCountText: String {
let count = cardDeck.unansweredCount
return "\(count) \(count == 1 ? "Card" : "Cards") Unattempted"
return "\(count) \(count == 1 ? "Card" : "Cards") Unanswered"
}
}
@ -146,7 +159,6 @@ extension CardDeckContainerView.ViewModel {
func record(_ answerState: Card.AnswerState, forCardAt index: Int) {
let card = visibleCards[index]
guard let managedObjectContext = card.managedObjectContext else { fatalError() }
card.answerState = answerState

View File

@ -11,6 +11,7 @@ import CypherPoetSwiftUIKit
struct CardDeckContainerView {
@EnvironmentObject var store: AppStore
@ObservedObject var viewModel: ViewModel
@State private var isShowingEditView = false
@ -64,11 +65,11 @@ extension CardDeckContainerView: View {
self.editDeckButton
}
}
.padding()
}
.padding()
}
}
.padding()
.padding()
.background(Color("CardDeckBackground"))
.edgesIgnoringSafeArea(.all)
.sheet(
@ -82,6 +83,13 @@ extension CardDeckContainerView: View {
viewModel: .init(currentDeck: self.viewModel.cardDeck)
)
}
.sheet(
isPresented: self.$isShowingSettingsView,
onDismiss: viewModel.resumeRound
) {
SettingsContainerView()
.environmentObject(self.store)
}
.onAppear {
self.viewModel.isTimerActive = true
}
@ -172,6 +180,7 @@ struct CardDeckContainerView_Previews: PreviewProvider {
viewModel: .init(cardDeck: PreviewData.CardDecks.default)
)
.environment(\.managedObjectContext, CurrentApp.coreDataManager.mainContext)
.environmentObject(PreviewData.AppStores.default)
// .previewLayout(PreviewLayout.iPhone11Landscape)
}
}

View File

@ -49,5 +49,6 @@ struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView()
.environment(\.managedObjectContext, CurrentApp.coreDataManager.mainContext)
.environmentObject(PreviewData.AppStores.default)
}
}

View File

@ -0,0 +1,46 @@
//
// SettingsContainerView.swift
// Flashzilla
//
// Created by CypherPoet on 1/22/20.
//
//
import SwiftUI
struct SettingsContainerView: View {
}
// MARK: - Body
extension SettingsContainerView {
var body: some View {
NavigationView {
SettingsFormView()
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
// MARK: - Computeds
extension SettingsContainerView {
}
// MARK: - View Variables
extension SettingsContainerView {
}
// MARK: - Preview
struct SettingsContainerView_Previews: PreviewProvider {
static var previews: some View {
SettingsContainerView()
.environmentObject(PreviewData.AppStores.default)
}
}

View File

@ -0,0 +1,74 @@
//
// SettingsFormView.swift
// Flashzilla
//
// Created by CypherPoet on 1/22/20.
//
//
import SwiftUI
struct SettingsFormView {
@Environment(\.presentationMode) private var presentationMode
@EnvironmentObject var store: AppStore
private var restacksIncorrectCards: Binding<Bool> {
store.binding(for: \.settingsState.restacksIncorrectCards) {
.settings(.restacksIncorrectCardsSet($0))
}
}
}
// MARK: - View
extension SettingsFormView: View {
var body: some View {
Form {
Section {
Toggle(isOn: restacksIncorrectCards) {
Text("Restack After Wrong Answers")
}
}
}
.navigationBarItems(trailing: doneButton)
.navigationBarTitle("Settings")
}
}
// MARK: - Computeds
extension SettingsFormView {
var settingsState: SettingsState { store.state.settingsState }
}
// MARK: - View Variables
extension SettingsFormView {
private var doneButton: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
}
}
}
// MARK: - Private Helpers
private extension SettingsFormView {
}
// MARK: - Preview
struct SettingsFormView_Previews: PreviewProvider {
static var previews: some View {
SettingsFormView()
.environmentObject(PreviewData.AppStores.default)
}
}