diff --git a/Bible.xcodeproj/xcuserdata/plarson.xcuserdatad/xcschemes/xcschememanagement.plist b/Bible.xcodeproj/xcuserdata/plarson.xcuserdatad/xcschemes/xcschememanagement.plist index e3cfd41..c6c86f3 100644 --- a/Bible.xcodeproj/xcuserdata/plarson.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Bible.xcodeproj/xcuserdata/plarson.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ Bible.xcscheme_^#shared#^_ orderHint - 1 + 0 SuppressBuildableAutocreation diff --git a/Bible/BibleApp.swift b/Bible/BibleApp.swift index d46bdc1..2105b06 100644 --- a/Bible/BibleApp.swift +++ b/Bible/BibleApp.swift @@ -19,6 +19,7 @@ final public class AppDelegate: NSObject, UIApplicationDelegate { ) ) { AppReducer() + ._printChanges() } public func application( @@ -32,6 +33,9 @@ final public class AppDelegate: NSObject, UIApplicationDelegate { @main struct BibleApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate + + var body: some Scene { WindowGroup { #if os(macOS) @@ -39,9 +43,7 @@ struct BibleApp: App { DesktopReader() }) #elseif os(iOS) - ReaderView(store: Store(initialState: Reader.State.init()) { - Reader() - }) + AppView(store: delegate.store) #else fatalError("Unsupported OS") #endif diff --git a/BibleCore/Package.swift b/BibleCore/Package.swift index 144aa67..da10c64 100644 --- a/BibleCore/Package.swift +++ b/BibleCore/Package.swift @@ -13,7 +13,8 @@ let package = Package( .library(name: "DirectoryCore", targets: ["DirectoryCore"]), .library(name: "UserDefaultsClient", targets: ["UserDefaultsClient"]), .library(name: "AppFeature", targets: ["AppFeature"]), - .library(name: "Classroom", targets: ["Classroom"]) + .library(name: "Classroom", targets: ["Classroom"]), + .library(name: "BibleComponents", targets: ["BibleComponents"]) ], dependencies: [ .package( @@ -30,6 +31,9 @@ let package = Package( ) ], targets: [ + .target( + name: "BibleComponents" + ), .target( name: "ReaderCore", dependencies: [ @@ -113,6 +117,7 @@ let package = Package( name: "AppFeature", dependencies: [ "ReaderCore", + "Classroom", .product(name: "ComposableArchitecture", package: "swift-composable-architecture") ] ), @@ -120,7 +125,9 @@ let package = Package( name: "Classroom", dependencies: [ "BibleCore", + "BibleComponents", "BibleClient", + "DirectoryCore", "UserDefaultsClient", .product(name: "WrappingHStack", package: "WrappingHStack"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture") diff --git a/BibleCore/Sources/AppFeature/App.swift b/BibleCore/Sources/AppFeature/App.swift index 98e7ccf..2c9748a 100644 --- a/BibleCore/Sources/AppFeature/App.swift +++ b/BibleCore/Sources/AppFeature/App.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import ReaderCore +import Classroom public struct AppReducer: Reducer { @@ -8,15 +9,17 @@ public struct AppReducer: Reducer { public enum State: Equatable { - case reader(Reader.State = .init()) - case empty + case classroom(Classroom.State = .init()) } public enum Action: Equatable { - case reader(Reader.Action) + case classroom(Classroom.Action) } public var body: some ReducerOf { + Scope(state: /State.classroom, action: /Action.classroom) { + Classroom() + } Reduce { state, action in .none } } } @@ -66,7 +69,7 @@ public struct AppReducer: Reducer { case .tabSelected(let tab): if tab == .read { - state.path.append(.empty) + state.path.append(.classroom()) } return .none diff --git a/BibleCore/Sources/AppFeature/AppView.swift b/BibleCore/Sources/AppFeature/AppView.swift index 505aa0e..e965784 100644 --- a/BibleCore/Sources/AppFeature/AppView.swift +++ b/BibleCore/Sources/AppFeature/AppView.swift @@ -1,18 +1,19 @@ import ComposableArchitecture +import Classroom import ReaderCore import SwiftUI -struct AppView: View { +public struct AppView: View { let store: StoreOf @ObservedObject var viewStore: ViewStoreOf - init(store: StoreOf) { + public init(store: StoreOf) { self.store = store self.viewStore = ViewStore(store, observe: { $0 }) } - var body: some View { + public var body: some View { NavigationStackStore( store.scope(state: \.path, action: AppReducer.Action.path) ) { @@ -33,15 +34,13 @@ struct AppView: View { } } destination: { initialState in switch initialState { - case .empty: - Text("hello") - .navigationBarBackButtonHidden(true) - case .reader: + case .classroom: CaseLet( - /AppReducer.Path.State.reader, - action: AppReducer.Path.Action.reader, - then: ReaderView.init(store:) + /AppReducer.Path.State.classroom, + action: AppReducer.Path.Action.classroom, + then: ClassroomView.init(store:) ) + .navigationBarBackButtonHidden(true) } } diff --git a/BibleCore/Sources/BibleComponents/ButtonStyle/CorrectButtonStyle.swift b/BibleCore/Sources/BibleComponents/ButtonStyle/CorrectButtonStyle.swift new file mode 100644 index 0000000..1287bf5 --- /dev/null +++ b/BibleCore/Sources/BibleComponents/ButtonStyle/CorrectButtonStyle.swift @@ -0,0 +1,42 @@ +import SwiftUI + +public struct CorrectButtonStyle: ButtonStyle { + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .kerning(0.5) + .textCase(.uppercase) + .offset(y: configuration.isPressed ? 0 : -4) + .font(.system(size: 14)) + .fontWeight(.bold) + .foregroundColor(.white) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill( + Color.correctGreen.shadow( + ShadowStyle.inner( + color: .darkGreen, + radius: 0, + x: 0, + y: configuration.isPressed ? 0 : -4 + ) + ) + ) + } + } +} + +public extension ButtonStyle where Self == CorrectButtonStyle { + static var correct: CorrectButtonStyle { CorrectButtonStyle() } +} + +struct BibleButton_Previews: PreviewProvider { + static var previews: some View { + Button("Correct") { + + } + .buttonStyle(.correct) + } +} diff --git a/BibleCore/Sources/BibleComponents/ButtonStyle/DisabledButtonStyle.swift b/BibleCore/Sources/BibleComponents/ButtonStyle/DisabledButtonStyle.swift new file mode 100644 index 0000000..fb0d825 --- /dev/null +++ b/BibleCore/Sources/BibleComponents/ButtonStyle/DisabledButtonStyle.swift @@ -0,0 +1,34 @@ +import SwiftUI + +public struct DisabledButtonStyle: ButtonStyle { + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .kerning(0.5) + .textCase(.uppercase) + .font(.system(size: 14)) + .fontWeight(.bold) + .foregroundColor(.softBlack) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill( + Color.softGray + ) + } + .disabled(true) + } +} + +public extension ButtonStyle where Self == DisabledButtonStyle { + static var disabled: DisabledButtonStyle { DisabledButtonStyle() } +} + +public struct DisabledButtonStylePreviews: PreviewProvider { + public static var previews: some View { + Button("disabled") { + + } + .buttonStyle(.disabled) + } +} diff --git a/BibleCore/Sources/BibleComponents/ButtonStyle/OptionButtonStyle.swift b/BibleCore/Sources/BibleComponents/ButtonStyle/OptionButtonStyle.swift new file mode 100644 index 0000000..3f1496e --- /dev/null +++ b/BibleCore/Sources/BibleComponents/ButtonStyle/OptionButtonStyle.swift @@ -0,0 +1,49 @@ +// +// SwiftUIView.swift +// +// +// Created by Peter Larson on 9/12/23. +// + +import SwiftUI + +public struct OptionButtonStyle: ButtonStyle { + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(size: 14)) + .fontWeight(.bold) + .foregroundColor(.softBlack) + .padding() + .overlay { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(lineWidth: 2.5) + .foregroundColor(.softGray) + } + .background { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill( + Color.white.shadow( + ShadowStyle.inner( + color: .softGray, + radius: 0, + x: 0, + y: -4 + ) + ) + ) + } + } +} + +public extension ButtonStyle where Self == OptionButtonStyle { + static var option: OptionButtonStyle { OptionButtonStyle() } +} + +struct OptionButtonStyle_Previews: PreviewProvider { + static var previews: some View { + Button("Word") { + + } + .buttonStyle(.option) + } +} diff --git a/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift b/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift new file mode 100644 index 0000000..91fe3b8 --- /dev/null +++ b/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift @@ -0,0 +1,38 @@ +import SwiftUI + +public struct UnselecedButtonStyle: ButtonStyle { + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .textCase(.uppercase) + .offset(y: configuration.isPressed ? 0 : -4) + .font(.system(size: 14)) + .fontWeight(.bold) + .foregroundColor(.softBlack) + .frame(height: 50) + .frame(maxWidth: .infinity) + .overlay { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(lineWidth: 2.5) + .foregroundColor(.softGray) + } + .background { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill( + Color.white.shadow( + ShadowStyle.inner( + color: .softGray, + radius: 0, + x: 0, + y: configuration.isPressed ? 0 : -4 + ) + ) + ) + } + } +} + +public extension ButtonStyle where Self == UnselecedButtonStyle { + static var unselected: UnselecedButtonStyle { UnselecedButtonStyle() } +} + + diff --git a/BibleCore/Sources/BibleComponents/Color+.swift b/BibleCore/Sources/BibleComponents/Color+.swift new file mode 100644 index 0000000..432ad98 --- /dev/null +++ b/BibleCore/Sources/BibleComponents/Color+.swift @@ -0,0 +1,22 @@ +import SwiftUI + +public extension Color { + init(hex: UInt, alpha: Double = 1) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xff) / 255, + green: Double((hex >> 08) & 0xff) / 255, + blue: Double((hex >> 00) & 0xff) / 255, + opacity: alpha + ) + } +} + +public extension Color { + static var softGray: Self { .init(hex: 0xE5E5E5) } + static var darkGray: Self { .init(hex: 0xAFAFAF) } + static var softBlack: Self { .init(hex: 0x3C3C3C) } + static var softGreen: Self { .init(hex: 0xD7FFB8) } + static var correctGreen: Self { .init(hex: 0x58CC02) } + static var darkGreen: Self { .init(hex: 0x58A700) } +} diff --git a/BibleCore/Sources/BibleComponents/ProgressBar.swift b/BibleCore/Sources/BibleComponents/ProgressBar.swift new file mode 100644 index 0000000..14a9a3d --- /dev/null +++ b/BibleCore/Sources/BibleComponents/ProgressBar.swift @@ -0,0 +1,36 @@ +import SwiftUI + +public struct ProgressBar: View { + + private let progress: Double + + public init(progress: Double) { + self.progress = progress + } + + public var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 16) + .fill(Color.softGray) + GeometryReader { proxy in + RoundedRectangle(cornerRadius: 16) + .fill(Color.correctGreen) + .frame(width: (proxy.size.width - 32) * progress + 32) +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(1/10)) +// .frame(width: (proxy.size.width - 32) * progress + 16) +// .frame(height: 8) +// .offset(x: 8, y: 2) + } + } + .frame(minWidth: 48) + .frame(height: 16) + } +} + +struct ProgressBar_Previews: PreviewProvider { + static var previews: some View { + ProgressBar(progress: 1.0) + .padding() + } +} diff --git a/BibleCore/Sources/BibleCore/Book.swift b/BibleCore/Sources/BibleCore/Book.swift index 211a499..ad7dc13 100644 --- a/BibleCore/Sources/BibleCore/Book.swift +++ b/BibleCore/Sources/BibleCore/Book.swift @@ -30,6 +30,10 @@ public extension Book { static var leviticus: Self { .init(id: 3, name: "Leviticus", testament: "ot") } + + static var john: Self { + .init(id: 43, name: "John", testament: "nt") + } } public extension Array where Element == Book { diff --git a/BibleCore/Sources/BibleCore/Verse.swift b/BibleCore/Sources/BibleCore/Verse.swift index 43f13ad..d2af7d3 100644 --- a/BibleCore/Sources/BibleCore/Verse.swift +++ b/BibleCore/Sources/BibleCore/Verse.swift @@ -28,10 +28,21 @@ public extension Verse { static var mock: Self { .init(id: 1, book: .genesis, chapterId: 1, verseId: 1, verse: "In the beginning God created the heavens and the earth.") } + + static var wept: Self { + .init(id: 2, book: .john, chapterId: 11, verseId: 35, verse: "Jesus wept.") + } } public extension Array where Element == Verse { static var mock: [Verse] { [.mock] } + + var complete: [String] { + self.map(\.verse) + .joined(separator: " ") + .split(separator: " ") + .map(String.init) + } } diff --git a/BibleCore/Sources/Classroom/Classroom.swift b/BibleCore/Sources/Classroom/Classroom.swift index bdaa7d6..07b353a 100644 --- a/BibleCore/Sources/Classroom/Classroom.swift +++ b/BibleCore/Sources/Classroom/Classroom.swift @@ -1,36 +1,107 @@ import ComposableArchitecture +import DirectoryCore import Foundation import UserDefaultsClient -struct Classroom: Reducer { +public struct Classroom: Reducer { + public struct Path: Reducer { + public enum State: Equatable, Codable { + case lesson(Lesson.State) + } + + public enum Action: Equatable { + case lesson(Lesson.Action) + } + + public var body: some ReducerOf { + Scope(state: /State.lesson, action: /Action.lesson) { + Lesson() + } + } + } - struct State: Equatable, Codable { + public init () {} + + public struct State: Equatable, Codable { var lessons: IdentifiedArrayOf = [] var selected: Lesson.State.ID? = nil + var directory: Directory.State? = nil + var path = StackState() + + @BindingState var isDirectoryOpen = false + + public init(lessons: IdentifiedArrayOf = [], selected: Lesson.State.ID? = nil, directory: Directory.State? = nil, isDirectoryOpen: Bool = false) { + self.lessons = lessons + self.selected = selected + self.directory = directory + self.isDirectoryOpen = isDirectoryOpen + } } - enum Action: Equatable { + public enum Action: BindableAction, Equatable { case task - case lesson(Lesson.Action) + case lesson(id: UUID, action: Lesson.Action) case select(id: UUID) + case openDirectory + case directory(Directory.Action) + case binding(_ action: BindingAction) + case path(StackAction) } @Dependency(\.defaults) var defaults: UserDefaultsClient - - var body: some ReducerOf { + public var body: some ReducerOf { + BindingReducer() Reduce { state, action in switch action { - case .task - return .run { - defaults.get - } + case .task: + return .none case .select(id: let id): state.selected = id + state.path.append(.lesson(state.lessons[id: id]!)) return .none case .lesson: return .none + case .openDirectory: + state.isDirectoryOpen = true + state.directory = Directory.State( + isDirectoryOpen: true, // TODO: refactor + books: [] + ) + return .none + case .directory(.book(id: _, action: .select(_, _, let verses, _))): + state.isDirectoryOpen = false + + var lesson: Lesson.State? = state.lessons.first { state in + state.verses.elementsEqual(verses) + } + + if let lesson = lesson { + state.selected = lesson.id + } else { + lesson = Lesson.State(verses: verses) + state.lessons.append(lesson!) + state.selected = lesson!.id + } + + return .none + case .directory: + return .none + case .binding: + return .none + case .path: + return .none } } + .ifLet(\.directory, action: /Action.directory) { + Directory() + } + .forEach(\.path, action: /Action.path) { + Path() + } + // MARK: - pretty sure I don't need this +// .forEach(\.lessons, action: /Action.lesson) { +// Lesson() +// } } } diff --git a/BibleCore/Sources/Classroom/ClassroomView.swift b/BibleCore/Sources/Classroom/ClassroomView.swift index 6093947..621f512 100644 --- a/BibleCore/Sources/Classroom/ClassroomView.swift +++ b/BibleCore/Sources/Classroom/ClassroomView.swift @@ -1,20 +1,82 @@ import ComposableArchitecture +import DirectoryCore import SwiftUI -struct ClassroomView: View { +public struct ClassroomView: View { + let store: StoreOf - var body: some View { - EmptyView() + @ObservedObject var viewStore: ViewStoreOf + + public init(store: StoreOf) { + self.store = store + self.viewStore = ViewStoreOf(store, observe: { $0 }) + } + + public var body: some View { + + NavigationStackStore(store.scope(state: \.path, action: Classroom.Action.path)) { + List { + ForEach(viewStore.lessons) { lesson in + Button { + viewStore.send(.select(id: lesson.id)) + } label: { + Text(lesson.id.description) + } + } + } + .listStyle(.inset) + .navigationTitle("Classroom") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + store.send(.openDirectory) + } label: { + Text("Add") + } + } + } + .popover(isPresented: viewStore.$isDirectoryOpen, content: { + IfLetStore( + store.scope( + state: \.directory, + action: Classroom.Action.directory + ), then: { store in + NavigationStack { + DirectoryView(store: store) + } + } + ) { + ProgressView() + } + }) + } destination: { store in + switch store { + case .lesson: + CaseLet( + /Classroom.Path.State.lesson, + action: Classroom.Path.Action.lesson, + then: LessonView.init(store:) + ) + .navigationBarBackButtonHidden() + // case .message(let text): + // Text(text) + + } + } } } struct ClassroomView_Previews: PreviewProvider { static var previews: some View { - ClassroomView( - store: Store(initialState: Classroom.State()) { - Classroom() - } - ) + NavigationStack { + ClassroomView( + store: Store(initialState: Classroom.State( + lessons: [.init(verses: .mock)] + )) { + Classroom() + } + ) + } } } diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift new file mode 100644 index 0000000..5f6b274 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift @@ -0,0 +1,80 @@ +import BibleCore +import ComposableArchitecture +import Foundation + +struct BuildByBabySteps: Reducer { + struct State: Equatable, Hashable, Codable { + var verses: [Verse] + var options: [String]? = nil + var currentPhrase: [String] = [] + + init(verses: [Verse], options: [String]? = nil, currentPhrase: [String] = []) { + + precondition(verses.complete.count != currentPhrase.count) + + self.verses = verses + self.options = options + self.currentPhrase = currentPhrase + } + } + + enum Action: Equatable { + case setup([Verse], [String]) + case guess(id: UUID) + } + + @Dependency(\.withRandomNumberGenerator) var withRandomNumberGenerator + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .guess(id: let id): + //state.currentPhrase.append(state.word) + + return .none + case .setup(let verses, let currentPhrases): + + state.verses = verses + state.currentPhrase = currentPhrases + + var options = [String]() + var copy = verses.complete + + // Correct option + options.append(copy.remove(at: currentPhrases.count)) + + // Add 1-3 other options if available + repeat { + withRandomNumberGenerator { + copy.shuffle(using: &$0) + } + + if let element = copy.popLast() { + options.append(element) + } + + } while !copy.isEmpty && options.count < 4 + + return .none + } + + } + } +} + +extension BuildByBabySteps.State: ExerciseProtocol { + var score: Int { + maxScore + } + + var isCorrect: Bool { + + return verses + .map(\.verse) + .joined(separator: " ") + .split(separator: " ") + .map(String.init) + .elementsEqual(currentPhrase) + + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabyStepsView.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabyStepsView.swift new file mode 100644 index 0000000..9f48601 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabyStepsView.swift @@ -0,0 +1,25 @@ +//import ComposableArchitecture +//import SwiftUI +// +//struct BuildByBabyStepsView: View { +// +// let store: StoreOf +// +// init(store: StoreOf) { +// self.store = store +// } +// +// var body: some View { +// Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) +// } +//} +// +//struct BuildByBabyStepsView_Previews: PreviewProvider { +// static var previews: some View { +// BuildByBabyStepsView( +// store: Store(initialState: BuildByBabySteps.State(verses: .mock)) { +// BuildByBabySteps() +// } +// ) +// } +//} diff --git a/BibleCore/Sources/Classroom/Lesson/FITB/FillInTheBlank.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift similarity index 54% rename from BibleCore/Sources/Classroom/Lesson/FITB/FillInTheBlank.swift rename to BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift index d78c4ba..937b28f 100644 --- a/BibleCore/Sources/Classroom/Lesson/FITB/FillInTheBlank.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift @@ -2,25 +2,21 @@ import ComposableArchitecture import BibleCore import BibleClient -/* - Types of learning modes - - FITB, fill in the blank - - In the beginning - - - - - */ - -struct FillInTheBlank: Reducer { - enum Difficulty: Equatable, Codable { - case easy, medium, hard - } +public struct BuildByLetter: Reducer { + public init () {} - struct State: Equatable, Codable { - var difficulty: Difficulty + public struct State: Equatable, Codable, Hashable, ExerciseProtocol { + var isCorrect: Bool { + false + } + + var score: Int { + 0 + } + var verses: [Verse] + var answer: [String?]? = nil + var wordBank: [String] = [] var correctAnswer: [String] { verses @@ -29,21 +25,28 @@ struct FillInTheBlank: Reducer { .split(separator: " ") .map(String.init) } - var answer: [String?] - var wordBank: [String] + + public init( + verses: [Verse], + answer: [String?]? = nil, + wordBank: [String] = [] + ) { + self.verses = verses + self.answer = answer + self.wordBank = wordBank + } } - enum Action { + public enum Action { case task } @Dependency(\.withRandomNumberGenerator) var withRandomNumberGenerator: WithRandomNumberGenerator - var body: some ReducerOf { + public var body: some ReducerOf { Reduce { state, action in switch action { case .task: - state.wordBank = withRandomNumberGenerator { state.correctAnswer.shuffled(using: &$0) } diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetterView.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetterView.swift new file mode 100644 index 0000000..e13ab32 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetterView.swift @@ -0,0 +1,29 @@ +import ComposableArchitecture +import SwiftUI + +struct BuildByLetterView: View { + + let store: StoreOf + + init(store: StoreOf) { + self.store = store + } + + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct BuildByLetterView_Previews: PreviewProvider { + static var previews: some View { + BuildByLetterView( + store: Store( + initialState: BuildByLetter.State( + verses: .mock + ) + ) { + BuildByLetter() + } + ) + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Piecemeal/Piecemeal.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift similarity index 65% rename from BibleCore/Sources/Classroom/Lesson/Piecemeal/Piecemeal.swift rename to BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift index 2e42b1b..1c4a49e 100644 --- a/BibleCore/Sources/Classroom/Lesson/Piecemeal/Piecemeal.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift @@ -1,15 +1,27 @@ import ComposableArchitecture import BibleCore import BibleClient +import Foundation -public struct Piecemeal: Reducer { +public struct BuildByWord: Reducer { public init() {} - public struct State: Equatable, Codable { + public struct State: Equatable, Codable, Hashable, ExerciseProtocol { + + public struct Guess: Equatable, Identifiable, Codable, Hashable { + let word: String + public let id: UUID + + init(word: String, id: UUID) { + self.word = word + self.id = id + } + } + var verses: [Verse]? = nil var error: ClassroomError? = nil - var wordBank = [String]() - var answer = [String]() + var wordBank = IdentifiedArrayOf() + var answer = IdentifiedArrayOf() var correctAnswer: [String] { guard let verses = verses else { @@ -27,10 +39,12 @@ public struct Piecemeal: Reducer { return false } - return correctAnswer.elementsEqual(answer) + return correctAnswer.elementsEqual(answer.map(\.word)) } - public struct ClassroomError: Equatable, Codable { + var score: Int { 0 } + + public struct ClassroomError: Equatable, Codable, Hashable { let title, message: String init(title: String, message: String) { @@ -44,12 +58,7 @@ public struct Piecemeal: Reducer { ) } - init( - verses: [Verse]? = nil, - error: ClassroomError? = nil, - wordBank: [String] = [String](), - answer: [String] = [String]() - ) { + init(verses: [Verse]? = nil, error: ClassroomError? = nil, wordBank: IdentifiedArrayOf = IdentifiedArrayOf(), answer: IdentifiedArrayOf = IdentifiedArrayOf()) { self.verses = verses self.error = error self.wordBank = wordBank @@ -61,12 +70,12 @@ public struct Piecemeal: Reducer { case task case setup([Verse]) case failedSetup(error: State.ClassroomError) - case guess(index: Int) - case check - case didComplete + case guess(id: UUID) + case remove(id: UUID) } @Dependency(\.bible) var bible: BibleClient + @Dependency(\.uuid) var uuid @Dependency(\.withRandomNumberGenerator) var withRandomNumberGenerator public var body: some ReducerOf { @@ -77,10 +86,9 @@ public struct Piecemeal: Reducer { // we want to go fetch a random verse to start learning from. guard state.verses == nil else { - return .none + return .send(.setup(state.verses!)) } - // Grab a random verse to learn return .run { send in guard @@ -103,9 +111,12 @@ public struct Piecemeal: Reducer { case .setup(let verses): state.verses = verses - state.wordBank = withRandomNumberGenerator { - state.correctAnswer.shuffled(using: &$0) - } + state.wordBank = IdentifiedArray(uniqueElements: withRandomNumberGenerator { (generator) -> [State.Guess] in + return state.correctAnswer.shuffled(using: &generator).map { (word) -> State.Guess in + return State.Guess.init(word: word, id: uuid()) + } + }) + state.answer = [] return .none @@ -114,20 +125,11 @@ public struct Piecemeal: Reducer { state.error = error return .none - case .guess(let index): - let guess = state.wordBank.remove(at: index) - - state.answer.append(guess) - - return .none - case .check: - if state.isCorrect { - return .send(.didComplete) - } - + case .guess(let id): + state.answer.append(state.wordBank.remove(id: id)!) return .none - case .didComplete: - print("Hazzah!") + case .remove(let id): + state.wordBank.append(state.answer.remove(id: id)!) return .none } } diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift new file mode 100644 index 0000000..f2e55f8 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift @@ -0,0 +1,67 @@ +import BibleComponents +import ComposableArchitecture +import SwiftUI + +import WrappingHStack + +public struct BuildByWordView: View { + public let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + @Namespace var namespace + + @State var foo = false + + public var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + VStack(alignment: .leading, spacing: 32) { + Text("Classroom") + .font(.largeTitle) + + WrappingHStack(alignment: .leading) { + ForEach(viewStore.answer) { guess in + Button(guess.word) { + viewStore.send(.remove(id: guess.id)) + } + .buttonStyle(.option) + .matchedGeometryEffect(id: guess.id, in: namespace, properties: .position) + } + } + + Spacer() + + WrappingHStack { + ForEach(viewStore.wordBank) { guess in + Button(guess.word) { + viewStore.send(.guess(id: guess.id), animation: .easeOut(duration: 0.3)) + } + .buttonStyle(.option) + .matchedGeometryEffect(id: guess.id, in: namespace, isSource: true) + } + } + + } + .task { viewStore.send(.task) } + .padding(.horizontal) + } + } +} + +struct BuildByWordView_Previews: PreviewProvider { + static var previews: some View { + BuildByWordView( + store: Store( + initialState: BuildByWord.State(), + reducer: { + BuildByWord() + .dependency(\.bible, .testValue) + ._printChanges() + } + ) + ) + .previewDevice("iPhone 14") + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift new file mode 100644 index 0000000..909e6c0 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift @@ -0,0 +1,44 @@ +import BibleCore +import ComposableArchitecture + +public struct Exercise: Reducer { + public init () {} + + public enum State: Equatable, Codable, Hashable, ExerciseProtocol { + var isCorrect: Bool { + switch self { + case .buildByLetter(let state): + return state.isCorrect + case .buildByWord(let state): + return state.isCorrect + } + } + + var score: Int { + switch self { + case .buildByLetter(let state): + return state.score + case .buildByWord(let state): + return state.score + } + + } + + case buildByWord(BuildByWord.State) + case buildByLetter(BuildByLetter.State) + } + + public enum Action: Equatable { + case buildByWord(BuildByWord.Action) + case buildByLetter(BuildByLetter.Action) + } + + public var body: some ReducerOf { + Scope(state: /State.buildByWord, action: /Action.buildByWord) { + BuildByWord() + } + Scope(state: /State.buildByLetter, action: /Action.buildByLetter) { + BuildByLetter() + } + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseProtocol.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseProtocol.swift new file mode 100644 index 0000000..05cd048 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseProtocol.swift @@ -0,0 +1,11 @@ +import Foundation + +protocol ExerciseProtocol { + var isCorrect: Bool { get } + var score: Int { get } + var maxScore: Int { get } +} + +extension ExerciseProtocol { + var maxScore: Int { return 10 } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseView.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseView.swift new file mode 100644 index 0000000..b0a6c81 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseView.swift @@ -0,0 +1,34 @@ +import ComposableArchitecture +import SwiftUI + +struct ExerciseView: View { + let store: StoreOf + + var body: some View { + SwitchStore(store) { initialState in + switch initialState { + case .buildByLetter: + CaseLet( + /Exercise.State.buildByLetter, action: Exercise.Action.buildByLetter, + then: BuildByLetterView.init(store:) + ) + case .buildByWord: + CaseLet( + /Exercise.State.buildByWord, action: Exercise.Action.buildByWord, + then: BuildByWordView.init(store:) + ) + } + } + } +} + +struct ExerciseView_Previews: PreviewProvider { + static var previews: some View { + ExerciseView( + store: Store(initialState: Exercise.State.buildByWord(.init(verses: .mock))) { + Exercise() + ._printChanges() + } + ) + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Exercise.swift b/BibleCore/Sources/Classroom/Lesson/Exercise.swift deleted file mode 100644 index 8579aa5..0000000 --- a/BibleCore/Sources/Classroom/Lesson/Exercise.swift +++ /dev/null @@ -1,21 +0,0 @@ -import BibleCore -import ComposableArchitecture - -struct Exercise: Reducer { - - enum State: Equatable, Codable { - case piecemeal(Piecemeal.State) - case fillInTheBlank(FillInTheBlank.State) - } - - enum Action: Equatable { - case piecemeal(Piecemeal.Action) - case fillInTheBlank(FillInTheBlank.Action) - } - - var body: some ReducerOf { - Reduce { state, action in - return .none - } - } -} diff --git a/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift new file mode 100644 index 0000000..578c6e5 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift @@ -0,0 +1,21 @@ +import ComposableArchitecture + +public struct Grade: Reducer { + public init () {} + + public enum State: Equatable, Codable, Hashable { + case disabled + case ready + case failed(String) + case partial(String) + case correct + } + + public enum Action: Equatable { + case didPressButton + } + + public var body: some ReducerOf { + Reduce { state, action in .none } + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift b/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift new file mode 100644 index 0000000..a1cb247 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift @@ -0,0 +1,132 @@ +import BibleComponents +import ComposableArchitecture +import SwiftUI + +fileprivate extension View { + + @ViewBuilder + func buttonStyle(for grade: Grade.State) -> some View { + switch grade { + case .correct: + self.buttonStyle(.correct) + case .disabled: + self.buttonStyle(.disabled) + case .ready: + self.buttonStyle(.correct) + case .failed(_): + self.buttonStyle(.unselected) + case .partial(_): + self.buttonStyle(.unselected) + } + } + + @ViewBuilder + func foreground(for grade: Grade.State) -> some View { + switch grade { + case .correct: + self.foregroundColor(.darkGreen) + case .disabled: + self.foregroundColor(.darkGreen) + case .ready: + self.foregroundColor(.darkGreen) + case .failed(_): + self.foregroundColor(.darkGreen) + case .partial(_): + self.foregroundColor(.darkGreen) + } + } + + @ViewBuilder + func hidden(for grade: Grade.State) -> some View { + switch grade { + case .correct: + self + default: + self.hidden() + } + } +} + +fileprivate extension String { + static func title(for grade: Grade.State) -> String { + switch grade { + case .correct: + return "Nice Job" + default: return String() + } + } + + static func text(for grade: Grade.State) -> String { + switch grade { + case .correct: + return "Next" + case .ready: + return "Check" + default: + return "Check" + } + } +} + +struct GradeView: View { + let store: StoreOf + + var body: some View { + SwitchStore(store) { initialState in + VStack(alignment: .leading) { + Text(String.title(for: initialState)) + .font(.system(size: 24)) + .fontWeight(.bold) + .foreground(for: initialState) + .hidden(for: initialState) + .transition(.move(edge: .bottom)) + Button(String.text(for: initialState)) { + store.send(.didPressButton, animation: .easeOut(duration: 0.3)) + } + .buttonStyle(for: initialState) + .transaction { $0.animation = nil } + } + .padding() + .background { + switch initialState { + case .correct: + Color.softGreen + .edgesIgnoringSafeArea(.bottom) + .transition(.move(edge: .bottom)) + default: + EmptyView() + } + } + } + } +} + +struct GradeView_Previews: PreviewProvider { + static var previews: some View { + + VStack { + Spacer() + GradeView(store: Store(initialState: .correct) { + Reduce { state, action in + +// state = .disabled + + return .none + } + }) + } + + + VStack { + Spacer() + GradeView(store: Store(initialState: .ready) { + Reduce { state, action in + + state = .correct + + return .none + } + }) + } + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Lesson.swift b/BibleCore/Sources/Classroom/Lesson/Lesson.swift index 4c15573..45a4de1 100644 --- a/BibleCore/Sources/Classroom/Lesson/Lesson.swift +++ b/BibleCore/Sources/Classroom/Lesson/Lesson.swift @@ -2,20 +2,88 @@ import BibleCore import ComposableArchitecture import Foundation -struct Lesson: Reducer { - struct State: Identifiable, Equatable, Codable { +public struct Lesson: Reducer { + public init () {} + + public struct State: Identifiable, Equatable, Codable, Hashable { var verses: [Verse] - var exercise: Exercise.State? - var id: UUID + var exercise: Exercise.State? = nil + var grade: Grade.State = .disabled + var score: Double = 0 + + public var id: UUID = UUID() + + public init( + verses: [Verse], + exercise: Exercise.State? = nil, + grade: Grade.State = .disabled, + id: UUID = UUID() + ) { + self.verses = verses + self.exercise = exercise + self.grade = grade + self.id = id + } } - enum Action: Equatable { + public enum Action: Equatable { + case prepare + case load(Exercise.State) case excercise(Exercise.Action) + case grade(Grade.Action) } - var body: some ReducerOf { - Reduce { state, action in + @Dependency(\.continuousClock) var clock + + public var body: some ReducerOf { + Scope(state: \.grade, action: /Action.grade) { + Grade() + } + Reduce { state, action in switch action { + case .prepare: + return .run { [verses = state.verses] send in + + try await clock.sleep(for: .seconds(1)) + + await send(.load(.buildByWord(BuildByWord.State.init(verses: verses))), animation: .easeIn(duration: 0.3)) + } + case .load(let exercise): + + state.score = .random(in: 0 ... 1) + state.exercise = exercise + + return .none + case .excercise(.buildByWord(BuildByWord.Action.guess)), .excercise(.buildByWord(.remove)): + if case .buildByWord(let model) = state.exercise { + if model.answer.isEmpty { + state.grade = Grade.State.disabled + } else { + state.grade = Grade.State.ready + } + } + return .none + case .grade(.didPressButton): + // Test if we've actually completed the exercise + + switch state.grade { + case .ready: + if state.exercise?.isCorrect ?? false { + state.grade = .correct + } + return .none + case .correct: + // Move to the next exercise. + state.exercise = nil + state.grade = .disabled + + return .send(.prepare) + default: + break + } + + + return .none default: return .none } diff --git a/BibleCore/Sources/Classroom/Lesson/LessonView.swift b/BibleCore/Sources/Classroom/Lesson/LessonView.swift new file mode 100644 index 0000000..5426eb2 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/LessonView.swift @@ -0,0 +1,67 @@ +import BibleComponents +import ComposableArchitecture +import SwiftUI + +struct LessonView: View { + + let store: StoreOf + + init(store: StoreOf) { + self.store = store + } + + var body: some View { + VStack { + HStack(spacing: 16) { + Button { + + } label: { + Image(systemName: "xmark") + } + .controlSize(.large) + .foregroundColor(.black) + + WithViewStore(store, observe: \.score) { + ProgressBar(progress: $0.state) + } + + } + .padding(.horizontal) + + IfLetStore( + store.scope(state: \.exercise, action: Lesson.Action.excercise), + then: ExerciseView.init(store:) + ) { + ProgressView() + .progressViewStyle(.circular) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + .transition( + .asymmetric( + insertion: .move(edge: .trailing), + removal: .move(edge: .leading) + ) + .combined(with: .opacity) + ) + + Spacer() + + GradeView(store: store.scope(state: \.grade, action: Lesson.Action.grade)) + } + .onAppear { + store.send(.prepare) + } + } +} + +struct LessonView_Previews: PreviewProvider { + static var previews: some View { + LessonView( + store: Store(initialState: Lesson.State.init(verses: .mock)) { + Lesson() + .dependency(\.bible, .testValue) + ._printChanges() + } + ) + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Piecemeal/PiecemealView.swift b/BibleCore/Sources/Classroom/Lesson/Piecemeal/PiecemealView.swift deleted file mode 100644 index f04e917..0000000 --- a/BibleCore/Sources/Classroom/Lesson/Piecemeal/PiecemealView.swift +++ /dev/null @@ -1,86 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -import WrappingHStack - -public struct PiecemealView: View { - public let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - VStack(alignment: .leading, spacing: 32) { - Text("Classroom") - .font(.largeTitle) - - WrappingHStack { - ForEach(viewStore.answer, id: \.self) { guess in - Text(guess) - } - } - - Spacer() - - if viewStore.wordBank.isEmpty { - - } else { - WrappingHStack { - ForEach(viewStore.wordBank.enumerated().map(\.offset), id: \.self) { index in - Button { - viewStore.send(.guess(index: index)) - } label: { - Text(viewStore.wordBank[index]) - .padding() - .foregroundColor(.black) - .background { - RoundedRectangle(cornerRadius: 12, style: .circular) - .stroke(lineWidth: 2) - .foregroundColor(Color.black.opacity(1/5)) - .shadow(color: Color.black.opacity(1/5), radius: 5, x: 5, y: 5) - } - } - - } - } - } - - Button { - viewStore.send(.check) - } label: { - Text("Check") - .textCase(.uppercase) - .foregroundColor(.white) - .fontWeight(.bold) - .frame(height: 50) - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: 12, style: .circular) - .foregroundColor(viewStore.answer.isEmpty ? .gray : .green) - } - } - .disabled(viewStore.answer.isEmpty) - - } - .task { viewStore.send(.task) } - .padding(.horizontal) - } - } -} - -struct PiecemealView_Previews: PreviewProvider { - static var previews: some View { - PiecemealView( - store: Store( - initialState: Piecemeal.State(), - reducer: { - Piecemeal() - .dependency(\.bible, .testValue) - } - ) - ) - .previewDevice("iPhone 14") - } -} diff --git a/BibleCore/Sources/DirectoryCore/Directory/Directory.swift b/BibleCore/Sources/DirectoryCore/Directory/Directory.swift index a2eb4e7..5b69703 100644 --- a/BibleCore/Sources/DirectoryCore/Directory/Directory.swift +++ b/BibleCore/Sources/DirectoryCore/Directory/Directory.swift @@ -8,12 +8,12 @@ public struct Directory: Reducer { // Do I really need to declare an explicit public initiallizer? public init() {} - public enum SortFilter: CaseIterable { + public enum SortFilter: CaseIterable, Codable { case traditional case alphabetical } - public struct State: Equatable { + public struct State: Equatable, Codable { public var isDirectoryOpen: Bool public var sections: IdentifiedArrayOf = [] public var focused: Book.ID? = nil diff --git a/BibleCore/Sources/DirectoryCore/Directory/Section/Section.swift b/BibleCore/Sources/DirectoryCore/Directory/Section/Section.swift index 3cd0c83..e1753c5 100644 --- a/BibleCore/Sources/DirectoryCore/Directory/Section/Section.swift +++ b/BibleCore/Sources/DirectoryCore/Directory/Section/Section.swift @@ -4,7 +4,7 @@ import SwiftUI import ComposableArchitecture public struct Section: Reducer { - public struct State: Equatable, Hashable, Identifiable { + public struct State: Equatable, Hashable, Identifiable, Codable { public var book: Book public var chapters: [Chapter] = [] public var chapter: Chapter? = nil diff --git a/BibleCore/Tests/ClassroomTests/ClassroomTests.swift b/BibleCore/Tests/ClassroomTests/ClassroomTests.swift index a878feb..75a9340 100644 --- a/BibleCore/Tests/ClassroomTests/ClassroomTests.swift +++ b/BibleCore/Tests/ClassroomTests/ClassroomTests.swift @@ -6,15 +6,16 @@ import XCTest @MainActor final class ClassroomTests: XCTestCase { - var store: TestStoreOf! + var store: TestStoreOf! var wordBank: [String]! override func setUp() async throws { - store = TestStore(initialState: Piecemeal.State()) { - Piecemeal() + store = TestStore(initialState: BuildByWord.State()) { + BuildByWord() } withDependencies: { $0.withRandomNumberGenerator = WithRandomNumberGenerator(LCRNG(seed: 0)) + $0.uuid = .incrementing } wordBank = ["In", "created", "the","heavens","the","God","and","the","earth.","beginning"] @@ -23,7 +24,11 @@ final class ClassroomTests: XCTestCase { await store.receive(.setup(.mock)) { $0.verses = .mock - $0.wordBank = self.wordBank + $0.wordBank = IdentifiedArray( + uniqueElements: self.wordBank.map { word in + BuildByWord.State.Guess(word: word, id: self.store.dependencies.uuid()) + } + ) } } @@ -33,50 +38,55 @@ final class ClassroomTests: XCTestCase { func testCorrectGuessing() async { var copy = store.state.wordBank - var correctGuessOrder = [Int]() + var correctOrder = [UUID]() - store.state.correctAnswer.forEach { - let index = copy.firstIndex(of: $0)! + store.state.correctAnswer.forEach { word in + let id = copy.first { guess in + guess.word == word + }?.id - copy.remove(at: index) + copy.remove(id: id!) - correctGuessOrder.append(index) + correctOrder.append(id!) } - var answer = [String]() + var answer = IdentifiedArrayOf() - for guess in correctGuessOrder { + for id in correctOrder { // Always fail until the last guess is made. XCTAssertFalse(store.state.isCorrect) - await store.send(.guess(index: guess)) { - let word = $0.wordBank.remove(at: guess) - answer.append(word) + await store.send(.guess(id: id)) { + let guess = $0.wordBank.remove(id: id) + + answer.append(guess!) + $0.answer = answer } } XCTAssertTrue(store.state.isCorrect) } - - func testIncorrectGuessing() async { - let incorrectGuesses = store.state.correctAnswer.map { _ in return 0 } - - var answer = [String]() - - for guess in incorrectGuesses { - // Always fail until the last guess is made. - XCTAssertFalse(store.state.isCorrect) - - await store.send(.guess(index: guess)) { - let word = $0.wordBank.remove(at: guess) - answer.append(word) - $0.answer = answer - } - } - - XCTAssertFalse(store.state.isCorrect) - } + // TODO: Re-implement +// +// func testIncorrectGuessing() async { +// let incorrectGuesses = store.state.correctAnswer.map { _ in return 0 } +// +// var answer = IdentifiedArrayOf() +// +// for id in incorrectGuesses { +// // Always fail until the last guess is made. +// XCTAssertFalse(store.state.isCorrect) +// +// await store.send(.guess(id: id)) { +// let word = $0.wordBank.remove(id: id) +// answer.append(word) +// $0.answer = answer +// } +// } +// +// XCTAssertFalse(store.state.isCorrect) +// } }