From d9e22f113732ddfb72cb43f482e755cd8bbf4c19 Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Fri, 8 Sep 2023 09:43:33 -0500 Subject: [PATCH 01/11] Classroom, Exercise, Lesson, Grade init --- .../xcschemes/xcschememanagement.plist | 2 +- BibleCore/Package.swift | 1 + BibleCore/Sources/Classroom/Classroom.swift | 16 ++++++++----- .../Sources/Classroom/ClassroomView.swift | 4 +++- .../BuildByLetter/BuildByLetter.swift} | 14 +---------- .../BuildByWord/BuildByWord.swift} | 2 +- .../BuildByWord/BuildByWordView.swift} | 14 +++++------ .../Classroom/Lesson/Excercise/Exercise.swift | 23 ++++++++++++++++++ .../Sources/Classroom/Lesson/Exercise.swift | 21 ---------------- .../Sources/Classroom/Lesson/Grade.swift | 24 +++++++++++++++++++ .../Sources/Classroom/Lesson/Lesson.swift | 20 +++++++++++++++- 11 files changed, 90 insertions(+), 51 deletions(-) rename BibleCore/Sources/Classroom/Lesson/{FITB/FillInTheBlank.swift => Excercise/BuildByLetter/BuildByLetter.swift} (88%) rename BibleCore/Sources/Classroom/Lesson/{Piecemeal/Piecemeal.swift => Excercise/BuildByWord/BuildByWord.swift} (99%) rename BibleCore/Sources/Classroom/Lesson/{Piecemeal/PiecemealView.swift => Excercise/BuildByWord/BuildByWordView.swift} (90%) create mode 100644 BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift delete mode 100644 BibleCore/Sources/Classroom/Lesson/Exercise.swift create mode 100644 BibleCore/Sources/Classroom/Lesson/Grade.swift 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/BibleCore/Package.swift b/BibleCore/Package.swift index 144aa67..e70925d 100644 --- a/BibleCore/Package.swift +++ b/BibleCore/Package.swift @@ -121,6 +121,7 @@ let package = Package( dependencies: [ "BibleCore", "BibleClient", + "DirectoryCore", "UserDefaultsClient", .product(name: "WrappingHStack", package: "WrappingHStack"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture") diff --git a/BibleCore/Sources/Classroom/Classroom.swift b/BibleCore/Sources/Classroom/Classroom.swift index bdaa7d6..52e4bf0 100644 --- a/BibleCore/Sources/Classroom/Classroom.swift +++ b/BibleCore/Sources/Classroom/Classroom.swift @@ -1,30 +1,34 @@ import ComposableArchitecture +import DirectoryCore import Foundation import UserDefaultsClient struct Classroom: Reducer { - struct State: Equatable, Codable { var lessons: IdentifiedArrayOf = [] var selected: Lesson.State.ID? = nil + var directory: Directory.State? = nil } enum Action: Equatable { case task case lesson(Lesson.Action) case select(id: UUID) + case openDirectory } @Dependency(\.defaults) var defaults: UserDefaultsClient - var body: some ReducerOf { Reduce { state, action in switch action { - case .task - return .run { - defaults.get - } + case .task: + return .none + case .openDirectory: + + + + return .none case .select(id: let id): state.selected = id return .none diff --git a/BibleCore/Sources/Classroom/ClassroomView.swift b/BibleCore/Sources/Classroom/ClassroomView.swift index 6093947..2165eec 100644 --- a/BibleCore/Sources/Classroom/ClassroomView.swift +++ b/BibleCore/Sources/Classroom/ClassroomView.swift @@ -5,7 +5,9 @@ struct ClassroomView: View { let store: StoreOf var body: some View { - EmptyView() + WithViewStore(store, observe: { $0 }) { viewStore in + + } } } diff --git a/BibleCore/Sources/Classroom/Lesson/FITB/FillInTheBlank.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift similarity index 88% rename from BibleCore/Sources/Classroom/Lesson/FITB/FillInTheBlank.swift rename to BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift index d78c4ba..4dd4dc4 100644 --- a/BibleCore/Sources/Classroom/Lesson/FITB/FillInTheBlank.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift @@ -2,18 +2,7 @@ import ComposableArchitecture import BibleCore import BibleClient -/* - Types of learning modes - - FITB, fill in the blank - - In the beginning - - - - - */ - -struct FillInTheBlank: Reducer { +struct BuildByLetter: Reducer { enum Difficulty: Equatable, Codable { case easy, medium, hard } @@ -43,7 +32,6 @@ struct FillInTheBlank: Reducer { Reduce { state, action in switch action { case .task: - state.wordBank = withRandomNumberGenerator { state.correctAnswer.shuffled(using: &$0) } diff --git a/BibleCore/Sources/Classroom/Lesson/Piecemeal/Piecemeal.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift similarity index 99% rename from BibleCore/Sources/Classroom/Lesson/Piecemeal/Piecemeal.swift rename to BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift index 2e42b1b..07bd84d 100644 --- a/BibleCore/Sources/Classroom/Lesson/Piecemeal/Piecemeal.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift @@ -2,7 +2,7 @@ import ComposableArchitecture import BibleCore import BibleClient -public struct Piecemeal: Reducer { +public struct BuildByWord: Reducer { public init() {} public struct State: Equatable, Codable { diff --git a/BibleCore/Sources/Classroom/Lesson/Piecemeal/PiecemealView.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift similarity index 90% rename from BibleCore/Sources/Classroom/Lesson/Piecemeal/PiecemealView.swift rename to BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift index f04e917..970c453 100644 --- a/BibleCore/Sources/Classroom/Lesson/Piecemeal/PiecemealView.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift @@ -3,10 +3,10 @@ import SwiftUI import WrappingHStack -public struct PiecemealView: View { - public let store: StoreOf +public struct BuildByWordView: View { + public let store: StoreOf - public init(store: StoreOf) { + public init(store: StoreOf) { self.store = store } @@ -70,13 +70,13 @@ public struct PiecemealView: View { } } -struct PiecemealView_Previews: PreviewProvider { +struct BuildByWordView_Previews: PreviewProvider { static var previews: some View { - PiecemealView( + BuildByWordView( store: Store( - initialState: Piecemeal.State(), + initialState: BuildByWord.State(), reducer: { - Piecemeal() + BuildByWord() .dependency(\.bible, .testValue) } ) diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift new file mode 100644 index 0000000..20e4789 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift @@ -0,0 +1,23 @@ +import BibleCore +import ComposableArchitecture + +struct Exercise: Reducer { + enum State: Equatable, Codable { + case buildByWord(BuildByWord.State) + case buildByLetter(BuildByLetter.State) + } + + enum Action: Equatable { + case buildByWord(BuildByWord.Action) + case buildByLetter(BuildByLetter.Action) + } + + 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/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.swift b/BibleCore/Sources/Classroom/Lesson/Grade.swift new file mode 100644 index 0000000..b720e55 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Grade.swift @@ -0,0 +1,24 @@ +import ComposableArchitecture + +struct Grade: Reducer { + enum State: Equatable, Codable { + case disabled + case ready + case failed(String) + case partial(String) + case correct + } + + enum Action: Equatable { + case submit + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .submit: + return .none + } + } + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Lesson.swift b/BibleCore/Sources/Classroom/Lesson/Lesson.swift index 4c15573..359eed5 100644 --- a/BibleCore/Sources/Classroom/Lesson/Lesson.swift +++ b/BibleCore/Sources/Classroom/Lesson/Lesson.swift @@ -5,15 +5,33 @@ import Foundation struct Lesson: Reducer { struct State: Identifiable, Equatable, Codable { var verses: [Verse] - var exercise: Exercise.State? + var exercise: Exercise.State? = nil + var grade: Grade.State = .disabled var id: UUID + + public init( + verses: [Verse], + exercise: Exercise.State? = nil, + grade: Grade.State, + id: UUID + ) { + self.verses = verses + self.exercise = exercise + self.grade = grade + self.id = id + } } enum Action: Equatable { + case prepare case excercise(Exercise.Action) + case grade(Grade.Action) } var body: some ReducerOf { + Scope(state: \.grade, action: /Action.grade) { + Grade() + } Reduce { state, action in switch action { default: From 364d02603efd64df2da3d28738672fbee1bab27d Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Fri, 8 Sep 2023 09:54:27 -0500 Subject: [PATCH 02/11] Codable --- BibleCore/Sources/DirectoryCore/Directory/Directory.swift | 4 ++-- .../Sources/DirectoryCore/Directory/Section/Section.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 From e54bd9f012c9660dc75036160cc2ba1814ff838c Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Tue, 12 Sep 2023 14:04:05 -0500 Subject: [PATCH 03/11] Classroom, BibleComponents, added Tests --- BibleCore/Package.swift | 7 +- .../ButtonStyle/CorrectButtonStyle.swift | 42 +++++++ .../ButtonStyle/DisabledButtonStyle.swift | 34 ++++++ .../ButtonStyle/OptionButtonStyle.swift | 49 ++++++++ .../ButtonStyle/UnselectedButtonStyle.swift | 37 ++++++ .../Sources/BibleComponents/Color+.swift | 22 ++++ .../Sources/BibleComponents/ProgressBar.swift | 20 ++++ BibleCore/Sources/BibleCore/Verse.swift | 7 ++ BibleCore/Sources/Classroom/Classroom.swift | 43 ++++++- .../Sources/Classroom/ClassroomView.swift | 50 ++++++++- .../BuildByBabySteps/BuildByBabySteps.swift | 79 +++++++++++++ .../BuildByBabyStepsView.swift | 25 +++++ .../BuildByLetter/BuildByLetter.swift | 20 ++-- .../BuildByLetter/BuildByLetterView.swift | 29 +++++ .../Excercise/BuildByWord/BuildByWord.swift | 18 +-- .../BuildByWord/BuildByWordView.swift | 44 ++------ .../Classroom/Lesson/Excercise/Exercise.swift | 2 +- .../Lesson/Excercise/ExerciseProtocol.swift | 11 ++ .../Lesson/Excercise/ExerciseView.swift | 34 ++++++ .../Classroom/Lesson/{ => Grade}/Grade.swift | 6 +- .../Classroom/Lesson/Grade/GradeView.swift | 106 ++++++++++++++++++ .../Sources/Classroom/Lesson/Lesson.swift | 15 ++- .../Sources/Classroom/Lesson/LessonView.swift | 54 +++++++++ .../Tests/ClassroomTests/ClassroomTests.swift | 6 +- 24 files changed, 684 insertions(+), 76 deletions(-) create mode 100644 BibleCore/Sources/BibleComponents/ButtonStyle/CorrectButtonStyle.swift create mode 100644 BibleCore/Sources/BibleComponents/ButtonStyle/DisabledButtonStyle.swift create mode 100644 BibleCore/Sources/BibleComponents/ButtonStyle/OptionButtonStyle.swift create mode 100644 BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift create mode 100644 BibleCore/Sources/BibleComponents/Color+.swift create mode 100644 BibleCore/Sources/BibleComponents/ProgressBar.swift create mode 100644 BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift create mode 100644 BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabyStepsView.swift create mode 100644 BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetterView.swift create mode 100644 BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseProtocol.swift create mode 100644 BibleCore/Sources/Classroom/Lesson/Excercise/ExerciseView.swift rename BibleCore/Sources/Classroom/Lesson/{ => Grade}/Grade.swift (81%) create mode 100644 BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift create mode 100644 BibleCore/Sources/Classroom/Lesson/LessonView.swift diff --git a/BibleCore/Package.swift b/BibleCore/Package.swift index e70925d..98a028b 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: [ @@ -120,6 +124,7 @@ let package = Package( name: "Classroom", dependencies: [ "BibleCore", + "BibleComponents", "BibleClient", "DirectoryCore", "UserDefaultsClient", 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..163f190 --- /dev/null +++ b/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift @@ -0,0 +1,37 @@ +import SwiftUI + +public struct UnselecedButtonStyle: ButtonStyle { + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .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..575de16 --- /dev/null +++ b/BibleCore/Sources/BibleComponents/ProgressBar.swift @@ -0,0 +1,20 @@ +// +// SwiftUIView.swift +// +// +// Created by Peter Larson on 9/12/23. +// + +import SwiftUI + +struct SwiftUIView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct SwiftUIView_Previews: PreviewProvider { + static var previews: some View { + SwiftUIView() + } +} diff --git a/BibleCore/Sources/BibleCore/Verse.swift b/BibleCore/Sources/BibleCore/Verse.swift index 43f13ad..54b6df6 100644 --- a/BibleCore/Sources/BibleCore/Verse.swift +++ b/BibleCore/Sources/BibleCore/Verse.swift @@ -34,4 +34,11 @@ 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 52e4bf0..6ddd57a 100644 --- a/BibleCore/Sources/Classroom/Classroom.swift +++ b/BibleCore/Sources/Classroom/Classroom.swift @@ -8,33 +8,66 @@ struct Classroom: Reducer { var lessons: IdentifiedArrayOf = [] var selected: Lesson.State.ID? = nil var directory: Directory.State? = nil + + @BindingState var isDirectoryOpen = false } - enum Action: Equatable { + 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) } @Dependency(\.defaults) var defaults: UserDefaultsClient var body: some ReducerOf { + BindingReducer() Reduce { state, action in switch action { case .task: return .none + case .select(id: let id): + state.selected = 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 .select(id: let id): - state.selected = id + case .directory: return .none - case .lesson: + case .binding: return .none } } + .ifLet(\.directory, action: /Action.directory) { + Directory() + } + .forEach(\.lessons, action: /Action.lesson) { + Lesson() + } } } diff --git a/BibleCore/Sources/Classroom/ClassroomView.swift b/BibleCore/Sources/Classroom/ClassroomView.swift index 2165eec..2061f60 100644 --- a/BibleCore/Sources/Classroom/ClassroomView.swift +++ b/BibleCore/Sources/Classroom/ClassroomView.swift @@ -1,12 +1,58 @@ import ComposableArchitecture +import DirectoryCore import SwiftUI struct ClassroomView: View { + let store: StoreOf + @ObservedObject var viewStore: ViewStoreOf + + init(store: StoreOf) { + self.store = store + self.viewStore = ViewStoreOf(store, observe: { $0 }) + } var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - + NavigationStack { + List { + ForEachStore( + store.scope( + state: \.lessons, + action: Classroom.Action.lesson(id:action:) + ), + content: { store in + Text("foo") + } + ) + } + .navigationDestination(for: Lesson.State.self, destination: { lesson in + Text("foo") + }) + .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() + } + }) } } } 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..4b5ff6f --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift @@ -0,0 +1,79 @@ +import BibleCore +import ComposableArchitecture + +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(String) + } + + @Dependency(\.withRandomNumberGenerator) var withRandomNumberGenerator + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .guess(let guess): + state.currentPhrase.append(guess) + + 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..5a39662 --- /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/Excercise/BuildByLetter/BuildByLetter.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift index 4dd4dc4..7007636 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift @@ -3,13 +3,11 @@ import BibleCore import BibleClient struct BuildByLetter: Reducer { - enum Difficulty: Equatable, Codable { - case easy, medium, hard - } - struct State: Equatable, Codable { - var difficulty: Difficulty + struct State: Equatable, Codable, Hashable { var verses: [Verse] + var answer: [String?]? = nil + var wordBank: [String] = [] var correctAnswer: [String] { verses @@ -18,8 +16,16 @@ struct BuildByLetter: Reducer { .split(separator: " ") .map(String.init) } - var answer: [String?] - var wordBank: [String] + + init( + verses: [Verse], + answer: [String?]? = nil, + wordBank: [String] = [] + ) { + self.verses = verses + self.answer = answer + self.wordBank = wordBank + } } enum Action { 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/Excercise/BuildByWord/BuildByWord.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift index 07bd84d..aae084f 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift @@ -5,7 +5,7 @@ import BibleClient public struct BuildByWord: Reducer { public init() {} - public struct State: Equatable, Codable { + public struct State: Equatable, Codable, Hashable { var verses: [Verse]? = nil var error: ClassroomError? = nil var wordBank = [String]() @@ -30,7 +30,7 @@ public struct BuildByWord: Reducer { return correctAnswer.elementsEqual(answer) } - public struct ClassroomError: Equatable, Codable { + public struct ClassroomError: Equatable, Codable, Hashable { let title, message: String init(title: String, message: String) { @@ -62,8 +62,6 @@ public struct BuildByWord: Reducer { case setup([Verse]) case failedSetup(error: State.ClassroomError) case guess(index: Int) - case check - case didComplete } @Dependency(\.bible) var bible: BibleClient @@ -77,10 +75,9 @@ public struct BuildByWord: 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 @@ -119,15 +116,6 @@ public struct BuildByWord: Reducer { state.answer.append(guess) - return .none - case .check: - if state.isCorrect { - return .send(.didComplete) - } - - return .none - case .didComplete: - print("Hazzah!") return .none } } diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift index 970c453..0f13f37 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift @@ -1,3 +1,4 @@ +import BibleComponents import ComposableArchitecture import SwiftUI @@ -24,44 +25,16 @@ public struct BuildByWordView: View { 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) - } - } - + WrappingHStack { + ForEach(viewStore.wordBank.enumerated().map(\.offset), id: \.self) { index in + + Button(viewStore.wordBank[index]) { + viewStore.send(.guess(index: index)) } + .buttonStyle(.option) + } } - - 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) } @@ -78,6 +51,7 @@ struct BuildByWordView_Previews: PreviewProvider { reducer: { BuildByWord() .dependency(\.bible, .testValue) + ._printChanges() } ) ) diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift index 20e4789..1e6ead9 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift @@ -2,7 +2,7 @@ import BibleCore import ComposableArchitecture struct Exercise: Reducer { - enum State: Equatable, Codable { + enum State: Equatable, Codable, Hashable { case buildByWord(BuildByWord.State) case buildByLetter(BuildByLetter.State) } 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/Grade.swift b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift similarity index 81% rename from BibleCore/Sources/Classroom/Lesson/Grade.swift rename to BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift index b720e55..a6a15fc 100644 --- a/BibleCore/Sources/Classroom/Lesson/Grade.swift +++ b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift @@ -1,7 +1,7 @@ import ComposableArchitecture struct Grade: Reducer { - enum State: Equatable, Codable { + enum State: Equatable, Codable, Hashable { case disabled case ready case failed(String) @@ -10,13 +10,13 @@ struct Grade: Reducer { } enum Action: Equatable { - case submit + case next } var body: some ReducerOf { Reduce { state, action in switch action { - case .submit: + case .next: return .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..3f2d605 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift @@ -0,0 +1,106 @@ +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(.unselected) + 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() + } + } +} + +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) + Button("Continue") { + store.send(.next) + } + .buttonStyle(for: initialState) + } + .padding() + .background { + switch initialState { + case .correct: + Color.softGreen + .transition(.move(edge: .bottom)) + .edgesIgnoringSafeArea(.bottom) + default: + EmptyView() + } + } + } + } +} + +struct GradeView_Previews: PreviewProvider { + static var previews: some View { + ForEach( + [Grade.State.correct, Grade.State.ready, Grade.State.disabled], + id: \.self + ) { grade in + VStack { + Spacer() + GradeView(store: Store(initialState: grade) { + Grade() + ._printChanges() + }) + } + } + } +} diff --git a/BibleCore/Sources/Classroom/Lesson/Lesson.swift b/BibleCore/Sources/Classroom/Lesson/Lesson.swift index 359eed5..01c82cc 100644 --- a/BibleCore/Sources/Classroom/Lesson/Lesson.swift +++ b/BibleCore/Sources/Classroom/Lesson/Lesson.swift @@ -3,17 +3,17 @@ import ComposableArchitecture import Foundation struct Lesson: Reducer { - struct State: Identifiable, Equatable, Codable { + struct State: Identifiable, Equatable, Codable, Hashable { var verses: [Verse] var exercise: Exercise.State? = nil var grade: Grade.State = .disabled - var id: UUID + var id: UUID = UUID() public init( verses: [Verse], exercise: Exercise.State? = nil, - grade: Grade.State, - id: UUID + grade: Grade.State = .disabled, + id: UUID = UUID() ) { self.verses = verses self.exercise = exercise @@ -34,6 +34,13 @@ struct Lesson: Reducer { } Reduce { state, action in switch action { + case .prepare: + + state.exercise = .buildByWord(BuildByWord.State.init(verses: .mock)) + + return .none + case .grade(.next): + 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..18e0fb1 --- /dev/null +++ b/BibleCore/Sources/Classroom/Lesson/LessonView.swift @@ -0,0 +1,54 @@ +import ComposableArchitecture +import SwiftUI + +struct LessonView: View { + + let store: StoreOf + + init(store: StoreOf) { + self.store = store + } + + var body: some View { + VStack { + HStack { + Button { + + } label: { + Image(systemName: "xmark") + } + + ProgressBar() + + } + + IfLetStore( + store.scope(state: \.exercise, action: Lesson.Action.excercise), + then: ExerciseView.init(store:) + ) { + ProgressView() + .progressViewStyle(.circular) + .frame(maxHeight: .infinity) + } + + 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/Tests/ClassroomTests/ClassroomTests.swift b/BibleCore/Tests/ClassroomTests/ClassroomTests.swift index a878feb..31f5cac 100644 --- a/BibleCore/Tests/ClassroomTests/ClassroomTests.swift +++ b/BibleCore/Tests/ClassroomTests/ClassroomTests.swift @@ -6,13 +6,13 @@ 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)) } From 8ae735fd56783c29c9e2fbeb379b7ceadbe6262b Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Wed, 13 Sep 2023 11:05:52 -0500 Subject: [PATCH 04/11] ProgressBar, UnselectedButtonStyle --- .../ButtonStyle/UnselectedButtonStyle.swift | 1 + .../Sources/BibleComponents/ProgressBar.swift | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift b/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift index 163f190..91fe3b8 100644 --- a/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift +++ b/BibleCore/Sources/BibleComponents/ButtonStyle/UnselectedButtonStyle.swift @@ -3,6 +3,7 @@ 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) diff --git a/BibleCore/Sources/BibleComponents/ProgressBar.swift b/BibleCore/Sources/BibleComponents/ProgressBar.swift index 575de16..14a9a3d 100644 --- a/BibleCore/Sources/BibleComponents/ProgressBar.swift +++ b/BibleCore/Sources/BibleComponents/ProgressBar.swift @@ -1,20 +1,36 @@ -// -// SwiftUIView.swift -// -// -// Created by Peter Larson on 9/12/23. -// - import SwiftUI -struct SwiftUIView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) +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 SwiftUIView_Previews: PreviewProvider { +struct ProgressBar_Previews: PreviewProvider { static var previews: some View { - SwiftUIView() + ProgressBar(progress: 1.0) + .padding() } } From a1ba21fd8a87ebdfeed79451b5e0fe09402ec23c Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Wed, 13 Sep 2023 11:06:09 -0500 Subject: [PATCH 05/11] BuildByWord, Lesson overhaul --- .../BuildByBabySteps/BuildByBabySteps.swift | 161 +++++++++--------- .../BuildByBabyStepsView.swift | 50 +++--- .../BuildByLetter/BuildByLetter.swift | 10 +- .../Excercise/BuildByWord/BuildByWord.swift | 52 +++--- .../BuildByWord/BuildByWordView.swift | 23 ++- .../Classroom/Lesson/Excercise/Exercise.swift | 21 ++- .../Classroom/Lesson/Grade/GradeView.swift | 4 +- .../Sources/Classroom/Lesson/Lesson.swift | 20 ++- .../Sources/Classroom/Lesson/LessonView.swift | 9 +- .../Tests/ClassroomTests/ClassroomTests.swift | 70 ++++---- 10 files changed, 250 insertions(+), 170 deletions(-) diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift index 4b5ff6f..3b071f6 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift @@ -1,79 +1,82 @@ -import BibleCore -import ComposableArchitecture - -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(String) - } - - @Dependency(\.withRandomNumberGenerator) var withRandomNumberGenerator - - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .guess(let guess): - state.currentPhrase.append(guess) - - 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) - - } -} +//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) +//// state.currentPhrase.append(guess) +//// state.currentPhrase.append(<#T##newElement: String##String#>) +// +// 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 index 5a39662..9f48601 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabyStepsView.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabyStepsView.swift @@ -1,25 +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() - } - ) - } -} +//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/Excercise/BuildByLetter/BuildByLetter.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift index 7007636..3f2634c 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift @@ -4,7 +4,15 @@ import BibleClient struct BuildByLetter: Reducer { - struct State: Equatable, Codable, Hashable { + struct State: Equatable, Codable, Hashable, ExerciseProtocol { + var isCorrect: Bool { + false + } + + var score: Int { + 0 + } + var verses: [Verse] var answer: [String?]? = nil var wordBank: [String] = [] diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift index aae084f..1c4a49e 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWord.swift @@ -1,15 +1,27 @@ import ComposableArchitecture import BibleCore import BibleClient +import Foundation public struct BuildByWord: Reducer { public init() {} - public struct State: Equatable, Codable, Hashable { + 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,9 +39,11 @@ public struct BuildByWord: Reducer { return false } - return correctAnswer.elementsEqual(answer) + return correctAnswer.elementsEqual(answer.map(\.word)) } + var score: Int { 0 } + public struct ClassroomError: Equatable, Codable, Hashable { let title, message: String @@ -44,12 +58,7 @@ public struct BuildByWord: 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,10 +70,12 @@ public struct BuildByWord: Reducer { case task case setup([Verse]) case failedSetup(error: State.ClassroomError) - case guess(index: Int) + 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 { @@ -100,9 +111,12 @@ public struct BuildByWord: 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 @@ -111,11 +125,11 @@ public struct BuildByWord: Reducer { state.error = error return .none - case .guess(let index): - let guess = state.wordBank.remove(at: index) - - state.answer.append(guess) - + case .guess(let id): + state.answer.append(state.wordBank.remove(id: id)!) + return .none + 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 index 0f13f37..f2e55f8 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByWord/BuildByWordView.swift @@ -11,28 +11,35 @@ public struct BuildByWordView: View { 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 { - ForEach(viewStore.answer, id: \.self) { guess in - Text(guess) + 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.enumerated().map(\.offset), id: \.self) { index in - - Button(viewStore.wordBank[index]) { - viewStore.send(.guess(index: index)) + 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) } } diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift index 1e6ead9..60e2d12 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift @@ -2,7 +2,26 @@ import BibleCore import ComposableArchitecture struct Exercise: Reducer { - enum State: Equatable, Codable, Hashable { + 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) } diff --git a/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift b/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift index 3f2d605..37b8da2 100644 --- a/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift +++ b/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift @@ -12,7 +12,7 @@ fileprivate extension View { case .disabled: self.buttonStyle(.disabled) case .ready: - self.buttonStyle(.unselected) + self.buttonStyle(.correct) case .failed(_): self.buttonStyle(.unselected) case .partial(_): @@ -68,7 +68,7 @@ struct GradeView: View { .fontWeight(.bold) .foreground(for: initialState) .hidden(for: initialState) - Button("Continue") { + Button("check") { store.send(.next) } .buttonStyle(for: initialState) diff --git a/BibleCore/Sources/Classroom/Lesson/Lesson.swift b/BibleCore/Sources/Classroom/Lesson/Lesson.swift index 01c82cc..5ead1be 100644 --- a/BibleCore/Sources/Classroom/Lesson/Lesson.swift +++ b/BibleCore/Sources/Classroom/Lesson/Lesson.swift @@ -32,14 +32,28 @@ struct Lesson: Reducer { Scope(state: \.grade, action: /Action.grade) { Grade() } - Reduce { state, action in + Reduce { state, action in switch action { case .prepare: - state.exercise = .buildByWord(BuildByWord.State.init(verses: .mock)) - + 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(.next): + // Test if we've actually completed the exercise + + + + + + return .none default: return .none diff --git a/BibleCore/Sources/Classroom/Lesson/LessonView.swift b/BibleCore/Sources/Classroom/Lesson/LessonView.swift index 18e0fb1..3f6b365 100644 --- a/BibleCore/Sources/Classroom/Lesson/LessonView.swift +++ b/BibleCore/Sources/Classroom/Lesson/LessonView.swift @@ -1,3 +1,4 @@ +import BibleComponents import ComposableArchitecture import SwiftUI @@ -11,16 +12,19 @@ struct LessonView: View { var body: some View { VStack { - HStack { + HStack(spacing: 16) { Button { } label: { Image(systemName: "xmark") } + .controlSize(.large) + .foregroundColor(.black) - ProgressBar() + ProgressBar(progress: 0) } + .padding(.horizontal) IfLetStore( store.scope(state: \.exercise, action: Lesson.Action.excercise), @@ -34,6 +38,7 @@ struct LessonView: View { Spacer() GradeView(store: store.scope(state: \.grade, action: Lesson.Action.grade)) + .transaction { $0.animation = nil } } .onAppear { store.send(.prepare) diff --git a/BibleCore/Tests/ClassroomTests/ClassroomTests.swift b/BibleCore/Tests/ClassroomTests/ClassroomTests.swift index 31f5cac..75a9340 100644 --- a/BibleCore/Tests/ClassroomTests/ClassroomTests.swift +++ b/BibleCore/Tests/ClassroomTests/ClassroomTests.swift @@ -15,6 +15,7 @@ final class ClassroomTests: XCTestCase { 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) +// } } From 870adc87afa511a2810cd7352a74ff63e181068b Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Wed, 13 Sep 2023 11:31:24 -0500 Subject: [PATCH 06/11] Path refactor --- BibleCore/Sources/AppFeature/App.swift | 11 +++++++---- BibleCore/Sources/AppFeature/AppView.swift | 19 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) 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) } } From a39e41ca6a2584822c76ef189e19e621cb60538d Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Wed, 13 Sep 2023 11:31:51 -0500 Subject: [PATCH 07/11] Added Classroom to AppFeature dependencies --- BibleCore/Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/BibleCore/Package.swift b/BibleCore/Package.swift index 98a028b..da10c64 100644 --- a/BibleCore/Package.swift +++ b/BibleCore/Package.swift @@ -117,6 +117,7 @@ let package = Package( name: "AppFeature", dependencies: [ "ReaderCore", + "Classroom", .product(name: "ComposableArchitecture", package: "swift-composable-architecture") ] ), From 6d39cd059414a52a34a95dfdda426591b6ea671b Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Wed, 13 Sep 2023 11:33:10 -0500 Subject: [PATCH 08/11] Updated main App entry to AppView --- Bible/BibleApp.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 From 486308700f83a625d6da2f7378bc3cd19072b802 Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Wed, 13 Sep 2023 11:33:42 -0500 Subject: [PATCH 09/11] Added public interface --- BibleCore/Sources/Classroom/Classroom.swift | 17 +++- .../Sources/Classroom/ClassroomView.swift | 91 ++++++++++--------- .../BuildByLetter/BuildByLetter.swift | 11 ++- .../Classroom/Lesson/Excercise/Exercise.swift | 10 +- .../Classroom/Lesson/Grade/Grade.swift | 10 +- .../Sources/Classroom/Lesson/Lesson.swift | 12 ++- 6 files changed, 84 insertions(+), 67 deletions(-) diff --git a/BibleCore/Sources/Classroom/Classroom.swift b/BibleCore/Sources/Classroom/Classroom.swift index 6ddd57a..0a70775 100644 --- a/BibleCore/Sources/Classroom/Classroom.swift +++ b/BibleCore/Sources/Classroom/Classroom.swift @@ -3,16 +3,25 @@ import DirectoryCore import Foundation import UserDefaultsClient -struct Classroom: Reducer { - struct State: Equatable, Codable { +public struct Classroom: Reducer { + public init () {} + + public struct State: Equatable, Codable { var lessons: IdentifiedArrayOf = [] var selected: Lesson.State.ID? = nil var directory: Directory.State? = nil @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: BindableAction, Equatable { + public enum Action: BindableAction, Equatable { case task case lesson(id: UUID, action: Lesson.Action) case select(id: UUID) @@ -23,7 +32,7 @@ struct Classroom: Reducer { @Dependency(\.defaults) var defaults: UserDefaultsClient - var body: some ReducerOf { + public var body: some ReducerOf { BindingReducer() Reduce { state, action in switch action { diff --git a/BibleCore/Sources/Classroom/ClassroomView.swift b/BibleCore/Sources/Classroom/ClassroomView.swift index 2061f60..248a883 100644 --- a/BibleCore/Sources/Classroom/ClassroomView.swift +++ b/BibleCore/Sources/Classroom/ClassroomView.swift @@ -2,67 +2,68 @@ import ComposableArchitecture import DirectoryCore import SwiftUI -struct ClassroomView: View { +public struct ClassroomView: View { let store: StoreOf + @ObservedObject var viewStore: ViewStoreOf - init(store: StoreOf) { + public init(store: StoreOf) { self.store = store self.viewStore = ViewStoreOf(store, observe: { $0 }) } - var body: some View { - NavigationStack { - List { - ForEachStore( - store.scope( - state: \.lessons, - action: Classroom.Action.lesson(id:action:) - ), - content: { store in - Text("foo") - } - ) - } - .navigationDestination(for: Lesson.State.self, destination: { lesson in - Text("foo") - }) - .listStyle(.inset) - .navigationTitle("Classroom") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - store.send(.openDirectory) - } label: { - Text("Add") - } + public var body: some View { + List { + ForEachStore( + store.scope( + state: \.lessons, + action: Classroom.Action.lesson(id:action:) + ), + content: { store in + Text("foo") + } + ) + } + .navigationDestination(for: Lesson.State.self, destination: { lesson in + Text("foo") + }) + .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) - } + } + .popover(isPresented: viewStore.$isDirectoryOpen, content: { + IfLetStore( + store.scope( + state: \.directory, + action: Classroom.Action.directory + ), then: { store in + NavigationStack { + DirectoryView(store: store) } - ) { - ProgressView() } - }) - } + ) { + ProgressView() + } + }) } } struct ClassroomView_Previews: PreviewProvider { static var previews: some View { - ClassroomView( - store: Store(initialState: Classroom.State()) { - Classroom() - } - ) + NavigationStack { + ClassroomView( + store: Store(initialState: Classroom.State()) { + Classroom() + } + ) + } } } diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift index 3f2634c..937b28f 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByLetter/BuildByLetter.swift @@ -2,9 +2,10 @@ import ComposableArchitecture import BibleCore import BibleClient -struct BuildByLetter: Reducer { +public struct BuildByLetter: Reducer { + public init () {} - struct State: Equatable, Codable, Hashable, ExerciseProtocol { + public struct State: Equatable, Codable, Hashable, ExerciseProtocol { var isCorrect: Bool { false } @@ -25,7 +26,7 @@ struct BuildByLetter: Reducer { .map(String.init) } - init( + public init( verses: [Verse], answer: [String?]? = nil, wordBank: [String] = [] @@ -36,13 +37,13 @@ struct BuildByLetter: Reducer { } } - 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: diff --git a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift index 60e2d12..909e6c0 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/Exercise.swift @@ -1,8 +1,10 @@ import BibleCore import ComposableArchitecture -struct Exercise: Reducer { - enum State: Equatable, Codable, Hashable, ExerciseProtocol { +public struct Exercise: Reducer { + public init () {} + + public enum State: Equatable, Codable, Hashable, ExerciseProtocol { var isCorrect: Bool { switch self { case .buildByLetter(let state): @@ -26,12 +28,12 @@ struct Exercise: Reducer { case buildByLetter(BuildByLetter.State) } - enum Action: Equatable { + public enum Action: Equatable { case buildByWord(BuildByWord.Action) case buildByLetter(BuildByLetter.Action) } - var body: some ReducerOf { + public var body: some ReducerOf { Scope(state: /State.buildByWord, action: /Action.buildByWord) { BuildByWord() } diff --git a/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift index a6a15fc..b04fca8 100644 --- a/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift +++ b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift @@ -1,7 +1,9 @@ import ComposableArchitecture -struct Grade: Reducer { - enum State: Equatable, Codable, Hashable { +public struct Grade: Reducer { + public init () {} + + public enum State: Equatable, Codable, Hashable { case disabled case ready case failed(String) @@ -9,11 +11,11 @@ struct Grade: Reducer { case correct } - enum Action: Equatable { + public enum Action: Equatable { case next } - var body: some ReducerOf { + public var body: some ReducerOf { Reduce { state, action in switch action { case .next: diff --git a/BibleCore/Sources/Classroom/Lesson/Lesson.swift b/BibleCore/Sources/Classroom/Lesson/Lesson.swift index 5ead1be..4e960b7 100644 --- a/BibleCore/Sources/Classroom/Lesson/Lesson.swift +++ b/BibleCore/Sources/Classroom/Lesson/Lesson.swift @@ -2,12 +2,14 @@ import BibleCore import ComposableArchitecture import Foundation -struct Lesson: Reducer { - struct State: Identifiable, Equatable, Codable, Hashable { +public struct Lesson: Reducer { + public init () {} + + public struct State: Identifiable, Equatable, Codable, Hashable { var verses: [Verse] var exercise: Exercise.State? = nil var grade: Grade.State = .disabled - var id: UUID = UUID() + public var id: UUID = UUID() public init( verses: [Verse], @@ -22,13 +24,13 @@ struct Lesson: Reducer { } } - enum Action: Equatable { + public enum Action: Equatable { case prepare case excercise(Exercise.Action) case grade(Grade.Action) } - var body: some ReducerOf { + public var body: some ReducerOf { Scope(state: \.grade, action: /Action.grade) { Grade() } From 5c5267af20337d086ff3606145ae9ae6db9f1493 Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Fri, 15 Sep 2023 14:48:26 -0500 Subject: [PATCH 10/11] Added mock --- BibleCore/Sources/BibleCore/Book.swift | 4 ++++ BibleCore/Sources/BibleCore/Verse.swift | 4 ++++ 2 files changed, 8 insertions(+) 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 54b6df6..d2af7d3 100644 --- a/BibleCore/Sources/BibleCore/Verse.swift +++ b/BibleCore/Sources/BibleCore/Verse.swift @@ -28,6 +28,10 @@ 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 { From 25d2c3c4979fc10175c6d78b5b4deec56f84a63a Mon Sep 17 00:00:00 2001 From: Peter Larson Date: Fri, 15 Sep 2023 14:48:40 -0500 Subject: [PATCH 11/11] Added Navigation --- BibleCore/Sources/Classroom/Classroom.swift | 29 +++- .../Sources/Classroom/ClassroomView.swift | 81 +++++---- .../BuildByBabySteps/BuildByBabySteps.swift | 162 +++++++++--------- .../Classroom/Lesson/Grade/Grade.swift | 9 +- .../Classroom/Lesson/Grade/GradeView.swift | 54 ++++-- .../Sources/Classroom/Lesson/Lesson.swift | 37 +++- .../Sources/Classroom/Lesson/LessonView.swift | 14 +- 7 files changed, 239 insertions(+), 147 deletions(-) diff --git a/BibleCore/Sources/Classroom/Classroom.swift b/BibleCore/Sources/Classroom/Classroom.swift index 0a70775..07b353a 100644 --- a/BibleCore/Sources/Classroom/Classroom.swift +++ b/BibleCore/Sources/Classroom/Classroom.swift @@ -4,12 +4,29 @@ import Foundation import UserDefaultsClient 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() + } + } + } + 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 @@ -28,6 +45,7 @@ public struct Classroom: Reducer { case openDirectory case directory(Directory.Action) case binding(_ action: BindingAction) + case path(StackAction) } @Dependency(\.defaults) var defaults: UserDefaultsClient @@ -40,6 +58,7 @@ public struct Classroom: Reducer { return .none case .select(id: let id): state.selected = id + state.path.append(.lesson(state.lessons[id: id]!)) return .none case .lesson: return .none @@ -70,13 +89,19 @@ public struct Classroom: Reducer { return .none case .binding: return .none + case .path: + return .none } } .ifLet(\.directory, action: /Action.directory) { Directory() } - .forEach(\.lessons, action: /Action.lesson) { - Lesson() + .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 248a883..621f512 100644 --- a/BibleCore/Sources/Classroom/ClassroomView.swift +++ b/BibleCore/Sources/Classroom/ClassroomView.swift @@ -14,45 +14,56 @@ public struct ClassroomView: View { } public var body: some View { - List { - ForEachStore( - store.scope( - state: \.lessons, - action: Classroom.Action.lesson(id:action:) - ), - content: { store in - Text("foo") + + 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) + } } - ) - } - .navigationDestination(for: Lesson.State.self, destination: { lesson in - Text("foo") - }) - .listStyle(.inset) - .navigationTitle("Classroom") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - store.send(.openDirectory) - } label: { - Text("Add") + } + .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) + .popover(isPresented: viewStore.$isDirectoryOpen, content: { + IfLetStore( + store.scope( + state: \.directory, + action: Classroom.Action.directory + ), then: { store in + NavigationStack { + DirectoryView(store: store) + } } + ) { + ProgressView() } - ) { - 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) + } - }) + } } } @@ -60,7 +71,9 @@ struct ClassroomView_Previews: PreviewProvider { static var previews: some View { NavigationStack { ClassroomView( - store: Store(initialState: Classroom.State()) { + 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 index 3b071f6..5f6b274 100644 --- a/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift +++ b/BibleCore/Sources/Classroom/Lesson/Excercise/BuildByBabySteps/BuildByBabySteps.swift @@ -1,82 +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) -//// state.currentPhrase.append(guess) -//// state.currentPhrase.append(<#T##newElement: String##String#>) -// -// 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) -// -// } -//} +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/Grade/Grade.swift b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift index b04fca8..578c6e5 100644 --- a/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift +++ b/BibleCore/Sources/Classroom/Lesson/Grade/Grade.swift @@ -12,15 +12,10 @@ public struct Grade: Reducer { } public enum Action: Equatable { - case next + case didPressButton } public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .next: - return .none - } - } + Reduce { state, action in .none } } } diff --git a/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift b/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift index 37b8da2..a1cb247 100644 --- a/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift +++ b/BibleCore/Sources/Classroom/Lesson/Grade/GradeView.swift @@ -55,6 +55,17 @@ fileprivate extension String { 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 { @@ -68,18 +79,20 @@ struct GradeView: View { .fontWeight(.bold) .foreground(for: initialState) .hidden(for: initialState) - Button("check") { - store.send(.next) + .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 - .transition(.move(edge: .bottom)) .edgesIgnoringSafeArea(.bottom) + .transition(.move(edge: .bottom)) default: EmptyView() } @@ -90,17 +103,30 @@ struct GradeView: View { struct GradeView_Previews: PreviewProvider { static var previews: some View { - ForEach( - [Grade.State.correct, Grade.State.ready, Grade.State.disabled], - id: \.self - ) { grade in - VStack { - Spacer() - GradeView(store: Store(initialState: grade) { - Grade() - ._printChanges() - }) - } + + 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 4e960b7..45a4de1 100644 --- a/BibleCore/Sources/Classroom/Lesson/Lesson.swift +++ b/BibleCore/Sources/Classroom/Lesson/Lesson.swift @@ -9,6 +9,8 @@ public struct Lesson: Reducer { var verses: [Verse] var exercise: Exercise.State? = nil var grade: Grade.State = .disabled + var score: Double = 0 + public var id: UUID = UUID() public init( @@ -26,10 +28,13 @@ public struct Lesson: Reducer { public enum Action: Equatable { case prepare + case load(Exercise.State) case excercise(Exercise.Action) case grade(Grade.Action) } + @Dependency(\.continuousClock) var clock + public var body: some ReducerOf { Scope(state: \.grade, action: /Action.grade) { Grade() @@ -37,7 +42,17 @@ public struct Lesson: Reducer { Reduce { state, action in switch action { case .prepare: - state.exercise = .buildByWord(BuildByWord.State.init(verses: .mock)) + 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 { @@ -48,12 +63,24 @@ public struct Lesson: Reducer { } } return .none - case .grade(.next): + 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 diff --git a/BibleCore/Sources/Classroom/Lesson/LessonView.swift b/BibleCore/Sources/Classroom/Lesson/LessonView.swift index 3f6b365..5426eb2 100644 --- a/BibleCore/Sources/Classroom/Lesson/LessonView.swift +++ b/BibleCore/Sources/Classroom/Lesson/LessonView.swift @@ -21,7 +21,9 @@ struct LessonView: View { .controlSize(.large) .foregroundColor(.black) - ProgressBar(progress: 0) + WithViewStore(store, observe: \.score) { + ProgressBar(progress: $0.state) + } } .padding(.horizontal) @@ -32,13 +34,19 @@ struct LessonView: View { ) { ProgressView() .progressViewStyle(.circular) - .frame(maxHeight: .infinity) + .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)) - .transaction { $0.animation = nil } } .onAppear { store.send(.prepare)