├── Flashcards ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── icon-40.png │ │ ├── icon-58.png │ │ ├── icon-60.png │ │ ├── icon-76.png │ │ ├── icon-80.png │ │ ├── icon-87.png │ │ ├── icon-1024.png │ │ ├── icon-120.png │ │ ├── icon-152.png │ │ ├── icon-167.png │ │ ├── icon-180.png │ │ └── Contents.json ├── FlashcardsApp.swift ├── AddDeckView.swift ├── Info.plist ├── EditDeckView.swift ├── Theme.swift ├── Deck.swift ├── AddCardView.swift ├── EditCardView.swift ├── CardView.swift ├── SettingsView.swift ├── Card.swift ├── ContentView.swift ├── DeckStore.swift └── DeckDetailView.swift ├── Flashcards.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── vb.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── Flashcards.xcscheme └── project.pbxproj ├── project.yml └── README.md /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-58.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-60.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-80.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-87.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-120.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-152.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-167.png -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards/Assets.xcassets/AppIcon.appiconset/icon-180.png -------------------------------------------------------------------------------- /Flashcards.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Flashcards.xcodeproj/project.xcworkspace/xcuserdata/vb.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vaibhavs10/flashcard-ios-app/main/Flashcards.xcodeproj/project.xcworkspace/xcuserdata/vb.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Flashcards/FlashcardsApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct FlashcardsApp: App { 5 | @StateObject private var store = DeckStore() 6 | 7 | var body: some Scene { 8 | WindowGroup { 9 | ContentView() 10 | .environmentObject(store) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: Flashcards 2 | options: 3 | bundleIdPrefix: com.vb 4 | deploymentTarget: 5 | iOS: 17.0 6 | targets: 7 | Flashcards: 8 | type: application 9 | platform: iOS 10 | sources: 11 | - path: Flashcards 12 | settings: 13 | PRODUCT_BUNDLE_IDENTIFIER: com.vb.flashcards 14 | INFOPLIST_FILE: Flashcards/Info.plist 15 | scheme: 16 | testTargets: [] 17 | schemes: 18 | Flashcards: 19 | build: 20 | targets: 21 | Flashcards: all 22 | run: 23 | config: Debug 24 | -------------------------------------------------------------------------------- /Flashcards/AddDeckView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AddDeckView: View { 4 | @Environment(\.dismiss) private var dismiss 5 | 6 | @State private var name: String = "" 7 | @State private var summary: String = "" 8 | 9 | let onSave: (String, String) -> Void 10 | 11 | var body: some View { 12 | NavigationStack { 13 | ZStack { 14 | AppTheme.background(for: colorScheme).ignoresSafeArea() 15 | 16 | Form { 17 | Section("Details") { 18 | TextField("Name", text: $name) 19 | TextField("Short description", text: $summary) 20 | } 21 | } 22 | .scrollContentBackground(.hidden) 23 | } 24 | .navigationTitle("New Deck") 25 | .toolbar { 26 | ToolbarItem(placement: .cancellationAction) { 27 | Button("Cancel") { dismiss() } 28 | } 29 | ToolbarItem(placement: .confirmationAction) { 30 | Button("Save") { 31 | onSave(name, summary) 32 | dismiss() 33 | } 34 | .disabled(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 35 | } 36 | } 37 | } 38 | .presentationDetents([.medium, .large]) 39 | .presentationDragIndicator(.visible) 40 | } 41 | 42 | @Environment(\.colorScheme) private var colorScheme 43 | } 44 | 45 | #Preview { 46 | AddDeckView { _, _ in } 47 | } 48 | -------------------------------------------------------------------------------- /Flashcards/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flashcards 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Flashcards 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchScreen 26 | 27 | UISupportedInterfaceOrientations 28 | 29 | UIInterfaceOrientationPortrait 30 | UIInterfaceOrientationLandscapeLeft 31 | UIInterfaceOrientationLandscapeRight 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Flashcards/EditDeckView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EditDeckView: View { 4 | @Environment(\.dismiss) private var dismiss 5 | 6 | @State private var name: String 7 | @State private var summary: String 8 | 9 | let onSave: (String, String) -> Void 10 | 11 | init(deck: Deck, onSave: @escaping (String, String) -> Void) { 12 | _name = State(initialValue: deck.name) 13 | _summary = State(initialValue: deck.summary) 14 | self.onSave = onSave 15 | } 16 | 17 | var body: some View { 18 | NavigationStack { 19 | ZStack { 20 | AppTheme.background(for: colorScheme).ignoresSafeArea() 21 | 22 | Form { 23 | Section("Details") { 24 | TextField("Name", text: $name) 25 | TextField("Short description", text: $summary) 26 | } 27 | } 28 | .scrollContentBackground(.hidden) 29 | } 30 | .navigationTitle("Edit Deck") 31 | .toolbar { 32 | ToolbarItem(placement: .cancellationAction) { 33 | Button("Cancel") { dismiss() } 34 | } 35 | ToolbarItem(placement: .confirmationAction) { 36 | Button("Save") { 37 | onSave(name, summary) 38 | dismiss() 39 | } 40 | .disabled(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 41 | } 42 | } 43 | } 44 | } 45 | 46 | @Environment(\.colorScheme) private var colorScheme 47 | } 48 | -------------------------------------------------------------------------------- /Flashcards/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "size": "20x20", 5 | "idiom": "iphone", 6 | "filename": "icon-40.png", 7 | "scale": "2x" 8 | }, 9 | { 10 | "size": "20x20", 11 | "idiom": "iphone", 12 | "filename": "icon-60.png", 13 | "scale": "3x" 14 | }, 15 | { 16 | "size": "29x29", 17 | "idiom": "iphone", 18 | "filename": "icon-58.png", 19 | "scale": "2x" 20 | }, 21 | { 22 | "size": "29x29", 23 | "idiom": "iphone", 24 | "filename": "icon-87.png", 25 | "scale": "3x" 26 | }, 27 | { 28 | "size": "40x40", 29 | "idiom": "iphone", 30 | "filename": "icon-80.png", 31 | "scale": "2x" 32 | }, 33 | { 34 | "size": "40x40", 35 | "idiom": "iphone", 36 | "filename": "icon-120.png", 37 | "scale": "3x" 38 | }, 39 | { 40 | "size": "60x60", 41 | "idiom": "iphone", 42 | "filename": "icon-120.png", 43 | "scale": "2x" 44 | }, 45 | { 46 | "size": "60x60", 47 | "idiom": "iphone", 48 | "filename": "icon-180.png", 49 | "scale": "3x" 50 | }, 51 | { 52 | "size": "76x76", 53 | "idiom": "ipad", 54 | "filename": "icon-76.png", 55 | "scale": "1x" 56 | }, 57 | { 58 | "size": "76x76", 59 | "idiom": "ipad", 60 | "filename": "icon-152.png", 61 | "scale": "2x" 62 | }, 63 | { 64 | "size": "83.5x83.5", 65 | "idiom": "ipad", 66 | "filename": "icon-167.png", 67 | "scale": "2x" 68 | }, 69 | { 70 | "size": "1024x1024", 71 | "idiom": "ios-marketing", 72 | "filename": "icon-1024.png", 73 | "scale": "1x" 74 | } 75 | ], 76 | "info": { 77 | "version": 1, 78 | "author": "xcode" 79 | } 80 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flashcards (Anki-lite) 2 | 3 | A minimal SwiftUI flashcard app for iOS with spaced repetition, multiple decks, and quick add/edit flows. 4 | 5 | ## Features 6 | - Decks with two templates: **Word/Definition** and **Sentence/Translation** 7 | - Spaced repetition (SM-2 lite): Again/Good/Easy, next-due scheduling, streak tracking 8 | - Study queue shows due + new cards; optional shuffle 9 | - Add, rename, delete decks; add/edit cards; backup/restore JSON 10 | - Light/dark theming, haptics, keyboard shortcuts (Space to flip, 1/2/3 grade) 11 | 12 | ## Project layout 13 | - `project.yml` — Xcodegen config 14 | - `Flashcards/` — app sources 15 | - `FlashcardsApp.swift` entry 16 | - `DeckStore.swift` data/persistence 17 | - `Deck.swift`, `Card.swift` models (SRS fields included) 18 | - Views: `ContentView`, `DeckDetailView`, `AddDeckView`, `AddCardView`, `EditDeckView`, `EditCardView`, `CardView`, `SettingsView` 19 | - `Theme.swift` styling helpers 20 | - `Info.plist` 21 | 22 | ## Build & Run 23 | 1) Generate the Xcode project 24 | ```sh 25 | xcodegen generate 26 | ``` 27 | 2) Build (simulator example) 28 | ```sh 29 | xcodebuild -scheme Flashcards -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build 30 | ``` 31 | 3) Run on device via Xcode: 32 | - Open `Flashcards.xcodeproj` 33 | - Set your Team under Signing (change bundle id if needed) 34 | - Select your iPhone in the run destination, press Run (⌘R) 35 | 36 | ## Usage tips 37 | - From Decks: swipe a deck → Edit; swipe delete to remove. 38 | - Inside a deck: tap card to flip; Space to flip, 1/2/3 for Again/Good/Easy; “Edit Card” to update current card. 39 | - Shuffle toggle sits beside the deck title. 40 | - Backups: Content toolbar → Settings → create/restore JSON backups (stored in Documents/Backups). 41 | 42 | ## Customizing 43 | - Colors/gradients: `Theme.swift` 44 | - Spaced-repetition tuning: adjust `Card.applyReview` (ease/interval logic) 45 | - Daily queue rules: `DeckStore.dueCards` / `newCards` 46 | 47 | ## Requirements 48 | - Xcode 15+ (iOS 17+ target) with iOS 26.1 SDK (or adjust deployment target in `project.yml`) 49 | - Swift 5.9+ 50 | 51 | ## License 52 | MIT (feel free to adapt). 53 | -------------------------------------------------------------------------------- /Flashcards/Theme.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum AppTheme { 4 | // Core palette tuned for both light and dark 5 | static let accent = Color(red: 0.29, green: 0.42, blue: 0.75) 6 | static let accentSoft = Color(red: 0.29, green: 0.42, blue: 0.75).opacity(0.12) 7 | static let surfaceLight = Color(red: 0.97, green: 0.98, blue: 1.0) 8 | static let surfaceDark = Color(red: 0.12, green: 0.13, blue: 0.16) 9 | 10 | static func background(for scheme: ColorScheme) -> LinearGradient { 11 | let top = scheme == .dark ? Color(red: 0.08, green: 0.1, blue: 0.13) : Color(red: 0.94, green: 0.96, blue: 0.99) 12 | let bottom = scheme == .dark ? Color(red: 0.03, green: 0.04, blue: 0.08) : Color.white 13 | return LinearGradient(colors: [top, bottom], startPoint: .topLeading, endPoint: .bottomTrailing) 14 | } 15 | 16 | static func cardBackground(for scheme: ColorScheme) -> some ShapeStyle { 17 | let base = scheme == .dark ? surfaceDark : surfaceLight 18 | return LinearGradient(colors: [base, base.opacity(0.92)], startPoint: .topLeading, endPoint: .bottomTrailing) 19 | } 20 | } 21 | 22 | struct CapsuleButtonStyle: ButtonStyle { 23 | func makeBody(configuration: Configuration) -> some View { 24 | configuration.label 25 | .font(.callout.weight(.semibold)) 26 | .padding(.horizontal, 16) 27 | .padding(.vertical, 10) 28 | .background(AppTheme.accent.opacity(configuration.isPressed ? 0.75 : 1), in: Capsule()) 29 | .foregroundStyle(.white) 30 | .shadow(color: .black.opacity(0.15), radius: configuration.isPressed ? 4 : 8, x: 0, y: 6) 31 | .scaleEffect(configuration.isPressed ? 0.98 : 1) 32 | .animation(.spring(response: 0.25, dampingFraction: 0.8), value: configuration.isPressed) 33 | } 34 | } 35 | 36 | struct SubtleCard: ViewModifier { 37 | @Environment(\.colorScheme) private var scheme 38 | 39 | func body(content: Content) -> some View { 40 | content 41 | .padding(16) 42 | .background(AppTheme.cardBackground(for: scheme), in: RoundedRectangle(cornerRadius: 18, style: .continuous)) 43 | .overlay( 44 | RoundedRectangle(cornerRadius: 18, style: .continuous) 45 | .stroke(AppTheme.accent.opacity(0.18), lineWidth: 1) 46 | ) 47 | .shadow(color: .black.opacity(scheme == .dark ? 0.35 : 0.1), radius: 18, x: 0, y: 12) 48 | } 49 | } 50 | 51 | extension View { 52 | func subtleCard() -> some View { modifier(SubtleCard()) } 53 | } 54 | -------------------------------------------------------------------------------- /Flashcards/Deck.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Deck: Identifiable, Codable, Hashable { 4 | var id: UUID = UUID() 5 | var name: String 6 | var summary: String 7 | var cards: [Card] = [] 8 | 9 | // Meta 10 | var timeSpentSeconds: TimeInterval = 0 11 | var lastStudyDay: Date? 12 | var currentStreak: Int = 0 13 | 14 | var count: Int { cards.count } 15 | var dueCount: Int { cards.filter { $0.isDue() }.count } 16 | var newCount: Int { cards.filter { $0.isNew }.count } 17 | var reviewCount: Int { cards.filter { !$0.isNew }.count } 18 | var averageEase: Double { 19 | guard !cards.isEmpty else { return 0 } 20 | return cards.map { $0.ease }.reduce(0, +) / Double(cards.count) 21 | } 22 | var accuracy: Double { 23 | let total = cards.reduce(0) { $0 + $1.totalReviews } 24 | guard total > 0 else { return 0 } 25 | let ok = cards.reduce(0) { $0 + $1.successfulReviews } 26 | return Double(ok) / Double(total) 27 | } 28 | 29 | mutating func addStudyTime(_ seconds: TimeInterval) { 30 | timeSpentSeconds += seconds 31 | } 32 | 33 | mutating func markStudied(on date: Date = Date()) { 34 | let day = Calendar.current.startOfDay(for: date) 35 | if let last = lastStudyDay { 36 | let diff = Calendar.current.dateComponents([.day], from: last, to: day).day ?? 0 37 | if diff == 1 { currentStreak += 1 } 38 | else if diff > 1 { currentStreak = 1 } 39 | } else { 40 | currentStreak = 1 41 | } 42 | lastStudyDay = day 43 | } 44 | 45 | // Codable defaults for backward compatibility 46 | enum CodingKeys: String, CodingKey { 47 | case id, name, summary, cards, timeSpentSeconds, lastStudyDay, currentStreak 48 | } 49 | 50 | init(id: UUID = UUID(), name: String, summary: String, cards: [Card] = []) { 51 | self.id = id 52 | self.name = name 53 | self.summary = summary 54 | self.cards = cards 55 | } 56 | 57 | init(from decoder: Decoder) throws { 58 | let container = try decoder.container(keyedBy: CodingKeys.self) 59 | id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID() 60 | name = try container.decode(String.self, forKey: .name) 61 | summary = try container.decode(String.self, forKey: .summary) 62 | cards = try container.decodeIfPresent([Card].self, forKey: .cards) ?? [] 63 | timeSpentSeconds = try container.decodeIfPresent(TimeInterval.self, forKey: .timeSpentSeconds) ?? 0 64 | lastStudyDay = try container.decodeIfPresent(Date.self, forKey: .lastStudyDay) 65 | currentStreak = try container.decodeIfPresent(Int.self, forKey: .currentStreak) ?? 0 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Flashcards/AddCardView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AddCardView: View { 4 | @Environment(\.dismiss) private var dismiss 5 | 6 | @State private var kind: CardKind = .word 7 | @State private var prompt: String = "" 8 | @State private var primary: String = "" 9 | @State private var secondary: String = "" 10 | 11 | let onSave: (Card) -> Void 12 | 13 | var body: some View { 14 | NavigationStack { 15 | ZStack { 16 | AppTheme.background(for: colorScheme).ignoresSafeArea() 17 | 18 | Form { 19 | Picker("Type", selection: $kind) { 20 | ForEach(CardKind.allCases) { kind in 21 | Text(kind.displayName).tag(kind) 22 | } 23 | } 24 | .pickerStyle(.segmented) 25 | 26 | Section(kind.promptLabel) { 27 | TextField(kind.promptLabel, text: $prompt, axis: .vertical) 28 | .lineLimit(2...4) 29 | } 30 | 31 | Section(kind.primaryLabel) { 32 | TextField(kind.primaryLabel, text: $primary, axis: .vertical) 33 | .lineLimit(2...4) 34 | } 35 | 36 | Section(kind.secondaryLabel) { 37 | TextField(kind.secondaryLabel, text: $secondary, axis: .vertical) 38 | .lineLimit(2...4) 39 | } 40 | } 41 | .scrollContentBackground(.hidden) 42 | } 43 | .navigationTitle("New Card") 44 | .toolbar { 45 | ToolbarItem(placement: .cancellationAction) { 46 | Button("Cancel") { dismiss() } 47 | } 48 | ToolbarItem(placement: .confirmationAction) { 49 | Button("Save") { 50 | let card = Card( 51 | kind: kind, 52 | prompt: prompt.trimmed, 53 | primary: primary.trimmedOptional, 54 | secondary: secondary.trimmedOptional 55 | ) 56 | onSave(card) 57 | dismiss() 58 | } 59 | .disabled(prompt.trimmed.isEmpty) 60 | } 61 | } 62 | } 63 | .presentationDetents([.medium, .large]) 64 | .presentationDragIndicator(.visible) 65 | } 66 | 67 | @Environment(\.colorScheme) private var colorScheme 68 | } 69 | 70 | private extension String { 71 | var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } 72 | var trimmedOptional: String? { 73 | let value = trimmed 74 | return value.isEmpty ? nil : value 75 | } 76 | } 77 | 78 | #Preview { 79 | AddCardView { _ in } 80 | } 81 | -------------------------------------------------------------------------------- /Flashcards/EditCardView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct EditCardView: View { 4 | @Environment(\.dismiss) private var dismiss 5 | 6 | @State private var kind: CardKind 7 | @State private var prompt: String 8 | @State private var primary: String 9 | @State private var secondary: String 10 | 11 | let onSave: (Card) -> Void 12 | let original: Card 13 | 14 | init(card: Card, onSave: @escaping (Card) -> Void) { 15 | _kind = State(initialValue: card.kind) 16 | _prompt = State(initialValue: card.prompt) 17 | _primary = State(initialValue: card.primary ?? "") 18 | _secondary = State(initialValue: card.secondary ?? "") 19 | self.onSave = onSave 20 | self.original = card 21 | } 22 | 23 | var body: some View { 24 | NavigationStack { 25 | ZStack { 26 | AppTheme.background(for: colorScheme).ignoresSafeArea() 27 | 28 | Form { 29 | Picker("Type", selection: $kind) { 30 | ForEach(CardKind.allCases) { kind in 31 | Text(kind.displayName).tag(kind) 32 | } 33 | } 34 | .pickerStyle(.segmented) 35 | 36 | Section(kind.promptLabel) { 37 | TextField(kind.promptLabel, text: $prompt, axis: .vertical) 38 | .lineLimit(2...4) 39 | } 40 | 41 | Section(kind.primaryLabel) { 42 | TextField(kind.primaryLabel, text: $primary, axis: .vertical) 43 | .lineLimit(2...4) 44 | } 45 | 46 | Section(kind.secondaryLabel) { 47 | TextField(kind.secondaryLabel, text: $secondary, axis: .vertical) 48 | .lineLimit(2...4) 49 | } 50 | } 51 | .scrollContentBackground(.hidden) 52 | } 53 | .navigationTitle("Edit Card") 54 | .toolbar { 55 | ToolbarItem(placement: .cancellationAction) { 56 | Button("Cancel") { dismiss() } 57 | } 58 | ToolbarItem(placement: .confirmationAction) { 59 | Button("Save") { 60 | var updated = original 61 | updated.kind = kind 62 | updated.prompt = prompt.trimmed 63 | updated.primary = primary.trimmedOptional 64 | updated.secondary = secondary.trimmedOptional 65 | onSave(updated) 66 | dismiss() 67 | } 68 | .disabled(prompt.trimmed.isEmpty) 69 | } 70 | } 71 | } 72 | } 73 | 74 | @Environment(\.colorScheme) private var colorScheme 75 | } 76 | 77 | private extension String { 78 | var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } 79 | var trimmedOptional: String? { 80 | let value = trimmed 81 | return value.isEmpty ? nil : value 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Flashcards/CardView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CardView: View { 4 | let card: Card 5 | let showingBack: Bool 6 | 7 | var body: some View { 8 | VStack(alignment: .leading, spacing: 18) { 9 | HStack { 10 | Label(card.kind.displayName, systemImage: card.kind == .word ? "textformat.abc" : "quote.opening") 11 | .font(.footnote.weight(.semibold)) 12 | .symbolRenderingMode(.hierarchical) 13 | .padding(.horizontal, 12) 14 | .padding(.vertical, 6) 15 | .background(AppTheme.accentSoft, in: Capsule()) 16 | .foregroundStyle(AppTheme.accent) 17 | Spacer() 18 | Image(systemName: showingBack ? "arrow.uturn.backward" : "sparkles") 19 | .foregroundStyle(.secondary) 20 | } 21 | 22 | Group { 23 | if showingBack, card.hasBackContent { 24 | VStack(alignment: .leading, spacing: 10) { 25 | if let primary = card.primary, !primary.isEmpty { 26 | Text(primary) 27 | .font(.title3.weight(.semibold)) 28 | .foregroundStyle(.primary) 29 | } 30 | if let secondary = card.secondary, !secondary.isEmpty { 31 | Text(secondary) 32 | .font(.body) 33 | .foregroundStyle(.secondary) 34 | } 35 | } 36 | } else { 37 | Text(card.prompt) 38 | .font(.title.weight(.semibold)) 39 | .foregroundStyle(.primary) 40 | .lineSpacing(2) 41 | } 42 | } 43 | .frame(maxWidth: .infinity, alignment: .leading) 44 | Spacer() 45 | } 46 | .padding(22) 47 | .frame(maxWidth: .infinity, maxHeight: .infinity) 48 | .background( 49 | RoundedRectangle(cornerRadius: 24, style: .continuous) 50 | .fill( 51 | LinearGradient(colors: [ 52 | AppTheme.accent.opacity(0.16), 53 | Color.clear 54 | ], startPoint: .topLeading, endPoint: .bottomTrailing) 55 | ) 56 | .background(AppTheme.cardBackground(for: colorScheme), in: RoundedRectangle(cornerRadius: 24, style: .continuous)) 57 | ) 58 | .overlay( 59 | RoundedRectangle(cornerRadius: 24, style: .continuous) 60 | .strokeBorder(AppTheme.accent.opacity(0.18), lineWidth: 1) 61 | ) 62 | .shadow(color: .black.opacity(0.12), radius: 16, x: 0, y: 10) 63 | .animation(.easeInOut(duration: 0.2), value: showingBack) 64 | .accessibilityElement(children: .combine) 65 | } 66 | 67 | @Environment(\.colorScheme) private var colorScheme 68 | } 69 | 70 | #Preview { 71 | CardView( 72 | card: Card(kind: .word, prompt: "ubiquitous", primary: "present everywhere", secondary: "Smartphones are ubiquitous."), 73 | showingBack: false 74 | ) 75 | .padding() 76 | } 77 | -------------------------------------------------------------------------------- /Flashcards.xcodeproj/xcshareddata/xcschemes/Flashcards.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Flashcards/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsView: View { 4 | @Environment(\.dismiss) private var dismiss 5 | @EnvironmentObject private var store: DeckStore 6 | @Binding var backupMessage: String? 7 | 8 | @State private var isWorking = false 9 | 10 | var body: some View { 11 | NavigationStack { 12 | Form { 13 | Section("Backups") { 14 | Button { 15 | Task { await createBackup() } 16 | } label: { 17 | Label("Create backup", systemImage: "externaldrive.badge.plus") 18 | } 19 | .disabled(isWorking) 20 | 21 | Button { 22 | Task { await restoreLatest() } 23 | } label: { 24 | Label("Restore latest", systemImage: "clock.arrow.circlepath") 25 | } 26 | .disabled(isWorking || store.listBackups().isEmpty) 27 | 28 | if let message = backupMessage { 29 | Text(message) 30 | .font(.caption) 31 | .foregroundStyle(.secondary) 32 | } 33 | } 34 | 35 | Section("Stats (all decks)") { 36 | let counts = store.decks.reduce(into: (cards: 0, due: 0, new: 0)) { result, deck in 37 | result.cards += deck.cards.count 38 | result.due += store.dueCards(for: deck).count 39 | result.new += store.newCards(for: deck).count 40 | } 41 | StatRow(label: "Total cards", value: counts.cards) 42 | StatRow(label: "Due today", value: counts.due) 43 | StatRow(label: "New", value: counts.new) 44 | StatRow(label: "Avg ease", value: String(format: "%.2f", store.decks.map { $0.averageEase }.reduce(0,+) / Double(max(store.decks.count,1)))) 45 | } 46 | } 47 | .navigationTitle("Settings") 48 | .toolbar { 49 | ToolbarItem(placement: .confirmationAction) { 50 | Button("Done") { dismiss() } 51 | } 52 | } 53 | } 54 | } 55 | 56 | private func createBackup() async { 57 | isWorking = true 58 | do { 59 | let url = try await store.createBackup() 60 | backupMessage = "Saved: \(url.lastPathComponent)" 61 | } catch { 62 | backupMessage = "Backup failed: \(error.localizedDescription)" 63 | } 64 | isWorking = false 65 | } 66 | 67 | private func restoreLatest() async { 68 | isWorking = true 69 | guard let latest = store.listBackups().first else { 70 | backupMessage = "No backups found" 71 | isWorking = false 72 | return 73 | } 74 | do { 75 | try await store.restore(from: latest) 76 | backupMessage = "Restored: \(latest.lastPathComponent)" 77 | } catch { 78 | backupMessage = "Restore failed: \(error.localizedDescription)" 79 | } 80 | isWorking = false 81 | } 82 | } 83 | 84 | private struct StatRow: View { 85 | let label: String 86 | let value: String 87 | 88 | init(label: String, value: Int) { 89 | self.label = label 90 | self.value = "\(value)" 91 | } 92 | 93 | init(label: String, value: String) { 94 | self.label = label 95 | self.value = value 96 | } 97 | 98 | var body: some View { 99 | HStack { 100 | Text(label) 101 | Spacer() 102 | Text(value) 103 | .foregroundStyle(.secondary) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Flashcards/Card.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | import Foundation 4 | 5 | enum CardKind: String, Codable, CaseIterable, Identifiable { 6 | case word 7 | case sentence 8 | 9 | var id: String { rawValue } 10 | var displayName: String { 11 | switch self { 12 | case .word: return "Word" 13 | case .sentence: return "Sentence" 14 | } 15 | } 16 | var promptLabel: String { 17 | switch self { 18 | case .word: return "Word" 19 | case .sentence: return "Sentence" 20 | } 21 | } 22 | var primaryLabel: String { 23 | switch self { 24 | case .word: return "Definition" 25 | case .sentence: return "Translation" 26 | } 27 | } 28 | var secondaryLabel: String { 29 | switch self { 30 | case .word: return "Example sentence" 31 | case .sentence: return "Notes (optional)" 32 | } 33 | } 34 | } 35 | 36 | enum ReviewQuality { 37 | case again, good, easy 38 | } 39 | 40 | struct Card: Identifiable, Codable, Hashable { 41 | var id: UUID = UUID() 42 | var kind: CardKind 43 | var prompt: String // word or sentence 44 | var primary: String? // definition or translation 45 | var secondary: String? // example sentence or notes 46 | 47 | // SRS fields 48 | var ease: Double = 2.5 49 | var interval: Int = 0 // days 50 | var repetitions: Int = 0 51 | var lapses: Int = 0 52 | var totalReviews: Int = 0 53 | var successfulReviews: Int = 0 54 | var createdAt: Date = Date() 55 | var lastReviewAt: Date? 56 | var due: Date = Date() // next due date (start of day) 57 | 58 | // MARK: - Review 59 | mutating func applyReview(_ quality: ReviewQuality, today: Date = Date()) { 60 | let q: Double 61 | switch quality { 62 | case .again: q = 1 63 | case .good: q = 3 64 | case .easy: q = 5 65 | } 66 | 67 | totalReviews += 1 68 | if quality != .again { successfulReviews += 1 } 69 | 70 | if q < 3 { 71 | lapses += 1 72 | repetitions = 0 73 | interval = 1 74 | } else { 75 | ease = max(1.3, ease + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))) 76 | repetitions += 1 77 | if repetitions == 1 { 78 | interval = 1 79 | } else if repetitions == 2 { 80 | interval = 6 81 | } else { 82 | interval = Int(round(Double(interval) * ease)) 83 | } 84 | } 85 | 86 | let startOfDay = Calendar.current.startOfDay(for: today) 87 | due = startOfDay.addingTimeInterval(86400 * Double(interval)) 88 | lastReviewAt = today 89 | } 90 | 91 | var isNew: Bool { totalReviews == 0 } 92 | 93 | func isDue(on date: Date = Date()) -> Bool { 94 | let startOfDay = Calendar.current.startOfDay(for: date) 95 | return due <= startOfDay 96 | } 97 | 98 | var accuracy: Double { 99 | guard totalReviews > 0 else { return 0 } 100 | return Double(successfulReviews) / Double(totalReviews) 101 | } 102 | 103 | var hasBackContent: Bool { 104 | (primary?.isEmpty == false) || (secondary?.isEmpty == false) 105 | } 106 | 107 | // MARK: - Codable with backward compatibility 108 | enum CodingKeys: String, CodingKey { 109 | case id, kind, prompt, primary, secondary, ease, interval, repetitions, lapses, totalReviews, successfulReviews, createdAt, lastReviewAt, due 110 | } 111 | 112 | init(id: UUID = UUID(), kind: CardKind, prompt: String, primary: String?, secondary: String?) { 113 | self.id = id 114 | self.kind = kind 115 | self.prompt = prompt 116 | self.primary = primary 117 | self.secondary = secondary 118 | self.due = Calendar.current.startOfDay(for: Date()) 119 | } 120 | 121 | init(from decoder: Decoder) throws { 122 | let container = try decoder.container(keyedBy: CodingKeys.self) 123 | id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID() 124 | kind = try container.decode(CardKind.self, forKey: .kind) 125 | prompt = try container.decode(String.self, forKey: .prompt) 126 | primary = try container.decodeIfPresent(String.self, forKey: .primary) 127 | secondary = try container.decodeIfPresent(String.self, forKey: .secondary) 128 | ease = try container.decodeIfPresent(Double.self, forKey: .ease) ?? 2.5 129 | interval = try container.decodeIfPresent(Int.self, forKey: .interval) ?? 0 130 | repetitions = try container.decodeIfPresent(Int.self, forKey: .repetitions) ?? 0 131 | lapses = try container.decodeIfPresent(Int.self, forKey: .lapses) ?? 0 132 | totalReviews = try container.decodeIfPresent(Int.self, forKey: .totalReviews) ?? 0 133 | successfulReviews = try container.decodeIfPresent(Int.self, forKey: .successfulReviews) ?? 0 134 | createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date() 135 | lastReviewAt = try container.decodeIfPresent(Date.self, forKey: .lastReviewAt) 136 | due = try container.decodeIfPresent(Date.self, forKey: .due) ?? Calendar.current.startOfDay(for: Date()) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Flashcards/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | @EnvironmentObject private var store: DeckStore 5 | @State private var showingAddDeck = false 6 | @State private var showingSettings = false 7 | @State private var backupMessage: String? 8 | @State private var editingDeck: Deck? 9 | 10 | var body: some View { 11 | NavigationStack { 12 | ZStack { 13 | AppTheme.background(for: colorScheme).ignoresSafeArea() 14 | 15 | List { 16 | ForEach(store.decks) { deck in 17 | NavigationLink(value: deck) { 18 | DeckRow(deck: deck) 19 | } 20 | .swipeActions(edge: .trailing, allowsFullSwipe: false) { 21 | Button(role: .destructive) { 22 | confirmDeleteDeck = deck 23 | showingDeleteAlert = true 24 | } label: { 25 | Label("Delete", systemImage: "trash") 26 | } 27 | 28 | Button("Edit") { 29 | editingDeck = deck 30 | } 31 | .tint(.blue) 32 | } 33 | } 34 | .onDelete { offsets in 35 | Task { await store.deleteDeck(at: offsets) } 36 | } 37 | } 38 | .listStyle(.insetGrouped) 39 | } 40 | .navigationTitle("Decks") 41 | .navigationDestination(for: Deck.self) { deck in 42 | DeckDetailView(deck: deck) 43 | } 44 | .toolbar { 45 | ToolbarItem(placement: .navigationBarLeading) { EditButton() } 46 | ToolbarItem(placement: .navigationBarTrailing) { 47 | Button { 48 | showingAddDeck = true 49 | } label: { 50 | Label("Add Deck", systemImage: "plus") 51 | } 52 | } 53 | ToolbarItem(placement: .bottomBar) { 54 | Button { 55 | showingSettings = true 56 | } label: { 57 | Label("Settings", systemImage: "gearshape") 58 | } 59 | } 60 | } 61 | } 62 | .sheet(isPresented: $showingAddDeck) { 63 | AddDeckView { name, summary in 64 | Task { await store.addDeck(name: name, summary: summary) } 65 | } 66 | .presentationDetents([.medium, .large]) 67 | .presentationDragIndicator(.visible) 68 | } 69 | .sheet(item: $editingDeck) { deck in 70 | EditDeckView(deck: deck) { name, summary in 71 | var updated = deck 72 | updated.name = name 73 | updated.summary = summary 74 | Task { await store.replace(updated) } 75 | } 76 | .presentationDetents([.medium, .large]) 77 | .presentationDragIndicator(.visible) 78 | } 79 | .sheet(isPresented: $showingSettings) { 80 | SettingsView(backupMessage: $backupMessage) 81 | .environmentObject(store) 82 | .presentationDetents([.medium, .large]) 83 | .presentationDragIndicator(.visible) 84 | } 85 | .alert("Delete deck?", isPresented: $showingDeleteAlert, presenting: confirmDeleteDeck) { deck in 86 | Button("Delete", role: .destructive) { 87 | if let idx = store.decks.firstIndex(where: { $0.id == deck.id }) { 88 | Task { await store.deleteDeck(at: IndexSet(integer: idx)) } 89 | } 90 | } 91 | Button("Cancel", role: .cancel) { } 92 | } message: { deck in 93 | Text("This will remove \"\(deck.name)\" and its cards.") 94 | } 95 | } 96 | 97 | @Environment(\.colorScheme) private var colorScheme 98 | @State private var confirmDeleteDeck: Deck? 99 | @State private var showingDeleteAlert = false 100 | } 101 | 102 | private struct DeckRow: View { 103 | let deck: Deck 104 | 105 | var body: some View { 106 | HStack(alignment: .firstTextBaseline, spacing: 12) { 107 | VStack(alignment: .leading, spacing: 6) { 108 | Text(deck.name) 109 | .font(.headline.weight(.semibold)) 110 | Text(deck.summary) 111 | .font(.subheadline) 112 | .foregroundStyle(.secondary) 113 | HStack(spacing: 8) { 114 | Label("\(deck.dueCount) due", systemImage: "clock") 115 | .font(.caption) 116 | .foregroundStyle(.secondary) 117 | if deck.currentStreak > 0 { 118 | Label("\(deck.currentStreak)d streak", systemImage: "flame") 119 | .font(.caption) 120 | .foregroundStyle(.orange) 121 | } 122 | } 123 | } 124 | Spacer() 125 | Text("\(deck.count)") 126 | .font(.footnote.weight(.semibold)) 127 | .foregroundStyle(.secondary) 128 | } 129 | .padding(.vertical, 6) 130 | } 131 | } 132 | 133 | #Preview { 134 | ContentView() 135 | .environmentObject(DeckStore()) 136 | } 137 | -------------------------------------------------------------------------------- /Flashcards/DeckStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | @MainActor 5 | final class DeckStore: ObservableObject { 6 | @Published private(set) var decks: [Deck] = [] 7 | 8 | private let fileURL: URL = { 9 | let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 10 | return documents.appendingPathComponent("decks.json") 11 | }() 12 | private let backupFolder: URL = { 13 | let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 14 | let folder = documents.appendingPathComponent("Backups", isDirectory: true) 15 | try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) 16 | return folder 17 | }() 18 | 19 | init() { 20 | Task { await load() } 21 | } 22 | 23 | func load() async { 24 | if let data = try? Data(contentsOf: fileURL), 25 | let decoded = try? JSONDecoder().decode([Deck].self, from: data) { 26 | await MainActor.run { self.decks = decoded } 27 | } else { 28 | await MainActor.run { self.decks = Self.seedData } 29 | await save() 30 | } 31 | } 32 | 33 | func addDeck(name: String, summary: String) async { 34 | var newDeck = Deck(name: name, summary: summary, cards: []) 35 | newDeck.cards.append(contentsOf: sampleCards(for: newDeck)) 36 | decks.append(newDeck) 37 | await save() 38 | } 39 | 40 | func addCard(_ card: Card, to deck: Deck) async { 41 | guard let index = decks.firstIndex(where: { $0.id == deck.id }) else { return } 42 | decks[index].cards.append(card) 43 | await save() 44 | } 45 | 46 | func updateCard(_ card: Card, in deck: Deck) async { 47 | guard let deckIndex = decks.firstIndex(where: { $0.id == deck.id }), 48 | let cardIndex = decks[deckIndex].cards.firstIndex(where: { $0.id == card.id }) else { return } 49 | decks[deckIndex].cards[cardIndex] = card 50 | await save() 51 | } 52 | 53 | func review(card: Card, in deck: Deck, quality: ReviewQuality) async { 54 | guard let deckIndex = decks.firstIndex(where: { $0.id == deck.id }), 55 | let cardIndex = decks[deckIndex].cards.firstIndex(where: { $0.id == card.id }) else { return } 56 | 57 | var updatedCard = decks[deckIndex].cards[cardIndex] 58 | updatedCard.applyReview(quality) 59 | decks[deckIndex].cards[cardIndex] = updatedCard 60 | decks[deckIndex].markStudied() 61 | await save() 62 | } 63 | 64 | func recordStudyTime(_ seconds: TimeInterval, for deck: Deck) async { 65 | guard let idx = decks.firstIndex(where: { $0.id == deck.id }) else { return } 66 | decks[idx].addStudyTime(seconds) 67 | await save() 68 | } 69 | 70 | func deleteDeck(at offsets: IndexSet) async { 71 | decks.remove(atOffsets: offsets) 72 | await save() 73 | } 74 | 75 | func replace(_ deck: Deck) async { 76 | guard let index = decks.firstIndex(where: { $0.id == deck.id }) else { return } 77 | decks[index] = deck 78 | await save() 79 | } 80 | 81 | func dueCards(for deck: Deck, today: Date = Date()) -> [Card] { 82 | let start = Calendar.current.startOfDay(for: today) 83 | return deck.cards.filter { $0.due <= start } 84 | } 85 | 86 | func newCards(for deck: Deck) -> [Card] { 87 | deck.cards.filter { $0.isNew } 88 | } 89 | 90 | func createBackup() async throws -> URL { 91 | guard let data = try? JSONEncoder().encode(decks) else { 92 | throw NSError(domain: "backup", code: 0, userInfo: [NSLocalizedDescriptionKey: "Encode failed"]) 93 | } 94 | let formatter = DateFormatter() 95 | formatter.dateFormat = "yyyyMMdd-HHmmss" 96 | let filename = "decks-\(formatter.string(from: Date())).json" 97 | let url = backupFolder.appendingPathComponent(filename) 98 | try data.write(to: url, options: [.atomic]) 99 | return url 100 | } 101 | 102 | func listBackups() -> [URL] { 103 | (try? FileManager.default.contentsOfDirectory(at: backupFolder, includingPropertiesForKeys: nil))? 104 | .sorted(by: { $0.lastPathComponent > $1.lastPathComponent }) ?? [] 105 | } 106 | 107 | func restore(from url: URL) async throws { 108 | let data = try Data(contentsOf: url) 109 | let decoded = try JSONDecoder().decode([Deck].self, from: data) 110 | decks = decoded 111 | await save() 112 | } 113 | 114 | private func save() async { 115 | guard let data = try? JSONEncoder().encode(decks) else { return } 116 | do { 117 | try data.write(to: fileURL, options: [.atomic]) 118 | } catch { 119 | print("Failed to save decks: \(error)") 120 | } 121 | } 122 | } 123 | 124 | extension DeckStore { 125 | static var seedData: [Deck] { 126 | [ 127 | Deck( 128 | name: "Daily Words", 129 | summary: "Mix of new vocabulary", 130 | cards: [ 131 | Card(kind: .word, prompt: "ubiquitous", primary: "present, appearing, or found everywhere", secondary: "Smartphones are ubiquitous in modern life."), 132 | Card(kind: .word, prompt: "cogent", primary: "clear, logical, and convincing", secondary: "She presented a cogent argument for renewable energy.") 133 | ] 134 | ), 135 | Deck( 136 | name: "Spanish Sentences", 137 | summary: "Everyday phrases", 138 | cards: [ 139 | Card(kind: .sentence, prompt: "¿Dónde está la estación?", primary: "Where is the station?", secondary: "Use when asking for directions."), 140 | Card(kind: .sentence, prompt: "Me gustaría un café, por favor.", primary: "I would like a coffee, please.", secondary: "Polite ordering phrase.") 141 | ] 142 | ) 143 | ] 144 | } 145 | 146 | func sampleCards(for deck: Deck) -> [Card] { [] } 147 | } 148 | -------------------------------------------------------------------------------- /Flashcards/DeckDetailView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DeckDetailView: View { 4 | let deck: Deck 5 | @EnvironmentObject private var store: DeckStore 6 | @Environment(\.colorScheme) private var colorScheme 7 | 8 | @State private var currentIndex: Int = 0 9 | @State private var showingBack: Bool = false 10 | @State private var showingAddCard: Bool = false 11 | @State private var editingCard: Card? 12 | @State private var randomOrder: Bool = false 13 | @State private var sessionStart: Date? 14 | 15 | private var liveDeck: Deck { 16 | store.decks.first(where: { $0.id == deck.id }) ?? deck 17 | } 18 | 19 | private var studyCards: [Card] { 20 | let due = store.dueCards(for: liveDeck) 21 | let fresh = store.newCards(for: liveDeck) 22 | let combined = due + fresh 23 | if randomOrder { 24 | return combined.shuffled() 25 | } 26 | return combined 27 | } 28 | 29 | var body: some View { 30 | ZStack { 31 | AppTheme.background(for: colorScheme).ignoresSafeArea() 32 | 33 | VStack(spacing: 20) { 34 | header 35 | 36 | if studyCards.isEmpty { 37 | ContentUnavailableView("No cards due", systemImage: "square.stack.3d.up.slash", description: Text("Add or wait for cards to become due.")) 38 | } else { 39 | CardView(card: currentCard, showingBack: showingBack) 40 | .onTapGesture { showingBack.toggle() } 41 | .animation(.spring(response: 0.3, dampingFraction: 0.8), value: studyCards) 42 | .frame(maxHeight: 360) 43 | 44 | gradeRow 45 | } 46 | 47 | Spacer() 48 | } 49 | .padding() 50 | } 51 | .navigationTitle(liveDeck.name) 52 | .navigationBarTitleDisplayMode(.inline) 53 | .toolbar { 54 | ToolbarItem(placement: .navigationBarTrailing) { 55 | Button { 56 | showingAddCard = true 57 | } label: { 58 | Label("Add Card", systemImage: "plus") 59 | } 60 | } 61 | ToolbarItem(placement: .navigationBarTrailing) { 62 | Menu { 63 | Toggle(isOn: $randomOrder) { 64 | Label("Shuffle", systemImage: "arrow.triangle.2.circlepath") 65 | } 66 | } label: { 67 | Image(systemName: "ellipsis.circle") 68 | } 69 | } 70 | ToolbarItem(placement: .bottomBar) { 71 | HStack(spacing: 12) { 72 | Button { 73 | haptic(.light) 74 | showingBack.toggle() 75 | } label: { 76 | Label(showingBack ? "Show Front" : "Show Back", systemImage: "rectangle.on.rectangle") 77 | } 78 | .buttonStyle(.borderedProminent) 79 | .tint(.accentColor) 80 | .keyboardShortcut(.space, modifiers: []) 81 | 82 | Button { 83 | editingCard = currentCard 84 | } label: { 85 | Label("Edit Card", systemImage: "pencil") 86 | } 87 | .buttonStyle(.bordered) 88 | } 89 | } 90 | } 91 | .sheet(isPresented: $showingAddCard) { 92 | AddCardView { card in 93 | Task { await store.addCard(card, to: liveDeck) } 94 | } 95 | } 96 | .sheet(item: $editingCard) { card in 97 | EditCardView(card: card) { updated in 98 | Task { await store.updateCard(updated, in: liveDeck) } 99 | } 100 | .presentationDetents([.medium, .large]) 101 | .presentationDragIndicator(.visible) 102 | } 103 | .onChange(of: studyCards.count) { _ in 104 | currentIndex = min(currentIndex, max(studyCards.count - 1, 0)) 105 | } 106 | .onAppear { 107 | sessionStart = Date() 108 | } 109 | .onDisappear { 110 | if let start = sessionStart { 111 | let elapsed = Date().timeIntervalSince(start) 112 | Task { await store.recordStudyTime(elapsed, for: liveDeck) } 113 | } 114 | } 115 | } 116 | 117 | private func moveCard(_ offset: Int) { 118 | guard !studyCards.isEmpty else { return } 119 | let newIndex = currentIndex + offset 120 | currentIndex = min(max(newIndex, 0), studyCards.count - 1) 121 | showingBack = false 122 | } 123 | 124 | private var currentCard: Card { 125 | studyCards[safe: currentIndex] ?? studyCards.first! 126 | } 127 | 128 | @ViewBuilder 129 | private var header: some View { 130 | VStack(alignment: .leading, spacing: 8) { 131 | Text(liveDeck.name) 132 | .font(.title2.bold()) 133 | 134 | HStack(spacing: 12) { 135 | StatPill(label: "Due", value: store.dueCards(for: liveDeck).count) 136 | StatPill(label: "New", value: store.newCards(for: liveDeck).count) 137 | StatPill(label: "Streak", value: liveDeck.currentStreak) 138 | } 139 | } 140 | .frame(maxWidth: .infinity, alignment: .leading) 141 | } 142 | 143 | private var gradeRow: some View { 144 | HStack(spacing: 12) { 145 | Button { 146 | haptic(.heavy) 147 | handleReview(.again) 148 | } label: { 149 | Label("Again", systemImage: "arrow.counterclockwise") 150 | } 151 | .buttonStyle(.borderedProminent) 152 | .tint(.red) 153 | .keyboardShortcut("1", modifiers: []) 154 | 155 | Button { 156 | haptic(.medium) 157 | handleReview(.good) 158 | } label: { 159 | Label("Good", systemImage: "checkmark") 160 | } 161 | .buttonStyle(.borderedProminent) 162 | .tint(.accentColor) 163 | .keyboardShortcut("2", modifiers: []) 164 | 165 | Button { 166 | haptic(.light) 167 | handleReview(.easy) 168 | } label: { 169 | Label("Easy", systemImage: "sparkles") 170 | } 171 | .buttonStyle(.borderedProminent) 172 | .tint(.green) 173 | .keyboardShortcut("3", modifiers: []) 174 | } 175 | } 176 | 177 | private func handleReview(_ quality: ReviewQuality) { 178 | Task { 179 | await store.review(card: currentCard, in: liveDeck, quality: quality) 180 | withAnimation { 181 | if currentIndex >= studyCards.count - 1 { 182 | currentIndex = max(0, studyCards.count - 2) 183 | } 184 | } 185 | showingBack = false 186 | } 187 | } 188 | 189 | private func haptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) { 190 | #if os(iOS) 191 | let generator = UIImpactFeedbackGenerator(style: style) 192 | generator.impactOccurred() 193 | #endif 194 | } 195 | } 196 | 197 | private extension Array where Element == Card { 198 | subscript(safe index: Int) -> Card? { 199 | guard indices.contains(index) else { return nil } 200 | return self[index] 201 | } 202 | } 203 | 204 | private struct StatPill: View { 205 | let label: String 206 | let value: Int 207 | 208 | var body: some View { 209 | HStack(spacing: 6) { 210 | Text(label) 211 | .font(.caption) 212 | Text("\(value)") 213 | .font(.footnote.weight(.semibold)) 214 | } 215 | .padding(.horizontal, 10) 216 | .padding(.vertical, 6) 217 | .background(AppTheme.accentSoft, in: Capsule()) 218 | .foregroundStyle(AppTheme.accent) 219 | } 220 | } 221 | 222 | #Preview { 223 | NavigationStack { 224 | DeckDetailView(deck: Deck(name: "Preview", summary: "", cards: DeckStore.seedData.first!.cards)) 225 | .environmentObject(DeckStore()) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Flashcards.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1D3B8506D046FAD46CE79849 /* FlashcardsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898ED96388E1C5A38EB3EC56 /* FlashcardsApp.swift */; }; 11 | 22EA8A37056DA9DF67942231 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EBD4239587D89700FDA1C2C /* Theme.swift */; }; 12 | 36B5049FB042105DBA5A689C /* EditCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 919A7167B048062917F0EA63 /* EditCardView.swift */; }; 13 | 7046D80BB7E05D4488B8E620 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 40590CEF9514F130D9EB7101 /* Assets.xcassets */; }; 14 | 7189A11FC08FA228D231E435 /* AddCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4488A88626ED805E8E577E /* AddCardView.swift */; }; 15 | 72F7708A3C39702559531067 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63139AAABC25634C88AAFFD6 /* Card.swift */; }; 16 | 750EBC5171F695D79333F75D /* AddDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 998BAE82A4D6E89866A2020F /* AddDeckView.swift */; }; 17 | 867A48F5177062A3ABB78274 /* DeckDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800C0B90FA3A835085D4C386 /* DeckDetailView.swift */; }; 18 | A8E27F73C8E678C996E113CA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0CFBF239CF41720591C315 /* ContentView.swift */; }; 19 | BAF4FB027959D46A8FAF10BB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31EE92FEF14FA11ECC57F9FC /* SettingsView.swift */; }; 20 | BC387B021D4B6DD3C3C31687 /* DeckStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEE915B264B0B42BDC1306B /* DeckStore.swift */; }; 21 | C2ACE02BAD0CB7F00EBED8DC /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061602331CB07B021DEDA3DA /* Deck.swift */; }; 22 | F300D933D57DE49DA00B8FDA /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565B494F222867B1F78EF1C5 /* CardView.swift */; }; 23 | F9A9AC3406052D6F830A9220 /* EditDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A16FD68B18CACA5FB97183 /* EditDeckView.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 061602331CB07B021DEDA3DA /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = ""; }; 28 | 070938971B6DA36EDC8B8668 /* Flashcards.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Flashcards.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 2EEE915B264B0B42BDC1306B /* DeckStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckStore.swift; sourceTree = ""; }; 30 | 31EE92FEF14FA11ECC57F9FC /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 31 | 3D0CFBF239CF41720591C315 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 32 | 3EBD4239587D89700FDA1C2C /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 33 | 40590CEF9514F130D9EB7101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | 565B494F222867B1F78EF1C5 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; 35 | 57D2CD70838F57E5188414E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 36 | 63139AAABC25634C88AAFFD6 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; 37 | 800C0B90FA3A835085D4C386 /* DeckDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckDetailView.swift; sourceTree = ""; }; 38 | 898ED96388E1C5A38EB3EC56 /* FlashcardsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlashcardsApp.swift; sourceTree = ""; }; 39 | 919A7167B048062917F0EA63 /* EditCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCardView.swift; sourceTree = ""; }; 40 | 93A16FD68B18CACA5FB97183 /* EditDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditDeckView.swift; sourceTree = ""; }; 41 | 998BAE82A4D6E89866A2020F /* AddDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddDeckView.swift; sourceTree = ""; }; 42 | BE4488A88626ED805E8E577E /* AddCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCardView.swift; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 687FF79CBD690C1BD72D7B86 /* Products */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 070938971B6DA36EDC8B8668 /* Flashcards.app */, 50 | ); 51 | name = Products; 52 | sourceTree = ""; 53 | }; 54 | 82A71E13467E00276E837B5A /* Flashcards */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | BE4488A88626ED805E8E577E /* AddCardView.swift */, 58 | 998BAE82A4D6E89866A2020F /* AddDeckView.swift */, 59 | 40590CEF9514F130D9EB7101 /* Assets.xcassets */, 60 | 63139AAABC25634C88AAFFD6 /* Card.swift */, 61 | 565B494F222867B1F78EF1C5 /* CardView.swift */, 62 | 3D0CFBF239CF41720591C315 /* ContentView.swift */, 63 | 061602331CB07B021DEDA3DA /* Deck.swift */, 64 | 800C0B90FA3A835085D4C386 /* DeckDetailView.swift */, 65 | 2EEE915B264B0B42BDC1306B /* DeckStore.swift */, 66 | 919A7167B048062917F0EA63 /* EditCardView.swift */, 67 | 93A16FD68B18CACA5FB97183 /* EditDeckView.swift */, 68 | 898ED96388E1C5A38EB3EC56 /* FlashcardsApp.swift */, 69 | 57D2CD70838F57E5188414E9 /* Info.plist */, 70 | 31EE92FEF14FA11ECC57F9FC /* SettingsView.swift */, 71 | 3EBD4239587D89700FDA1C2C /* Theme.swift */, 72 | ); 73 | path = Flashcards; 74 | sourceTree = ""; 75 | }; 76 | DBFB1E547D31343C7FD8370D = { 77 | isa = PBXGroup; 78 | children = ( 79 | 82A71E13467E00276E837B5A /* Flashcards */, 80 | 687FF79CBD690C1BD72D7B86 /* Products */, 81 | ); 82 | sourceTree = ""; 83 | }; 84 | /* End PBXGroup section */ 85 | 86 | /* Begin PBXNativeTarget section */ 87 | 2CC6795DD59D1A2501280D40 /* Flashcards */ = { 88 | isa = PBXNativeTarget; 89 | buildConfigurationList = CE79D582715594345ADD9246 /* Build configuration list for PBXNativeTarget "Flashcards" */; 90 | buildPhases = ( 91 | B5E77E6414FBCAEFBCAD1F6A /* Sources */, 92 | 602C9176F2311B36D7144862 /* Resources */, 93 | ); 94 | buildRules = ( 95 | ); 96 | dependencies = ( 97 | ); 98 | name = Flashcards; 99 | packageProductDependencies = ( 100 | ); 101 | productName = Flashcards; 102 | productReference = 070938971B6DA36EDC8B8668 /* Flashcards.app */; 103 | productType = "com.apple.product-type.application"; 104 | }; 105 | /* End PBXNativeTarget section */ 106 | 107 | /* Begin PBXProject section */ 108 | 99F96BF0A775FB829E9E9FEA /* Project object */ = { 109 | isa = PBXProject; 110 | attributes = { 111 | BuildIndependentTargetsInParallel = YES; 112 | LastUpgradeCheck = 1430; 113 | }; 114 | buildConfigurationList = 486C63B2BBDB4BA36F23F6AE /* Build configuration list for PBXProject "Flashcards" */; 115 | compatibilityVersion = "Xcode 14.0"; 116 | developmentRegion = en; 117 | hasScannedForEncodings = 0; 118 | knownRegions = ( 119 | Base, 120 | en, 121 | ); 122 | mainGroup = DBFB1E547D31343C7FD8370D; 123 | minimizedProjectReferenceProxies = 1; 124 | preferredProjectObjectVersion = 77; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 2CC6795DD59D1A2501280D40 /* Flashcards */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXResourcesBuildPhase section */ 134 | 602C9176F2311B36D7144862 /* Resources */ = { 135 | isa = PBXResourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | 7046D80BB7E05D4488B8E620 /* Assets.xcassets in Resources */, 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXResourcesBuildPhase section */ 143 | 144 | /* Begin PBXSourcesBuildPhase section */ 145 | B5E77E6414FBCAEFBCAD1F6A /* Sources */ = { 146 | isa = PBXSourcesBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | 7189A11FC08FA228D231E435 /* AddCardView.swift in Sources */, 150 | 750EBC5171F695D79333F75D /* AddDeckView.swift in Sources */, 151 | 72F7708A3C39702559531067 /* Card.swift in Sources */, 152 | F300D933D57DE49DA00B8FDA /* CardView.swift in Sources */, 153 | A8E27F73C8E678C996E113CA /* ContentView.swift in Sources */, 154 | C2ACE02BAD0CB7F00EBED8DC /* Deck.swift in Sources */, 155 | 867A48F5177062A3ABB78274 /* DeckDetailView.swift in Sources */, 156 | BC387B021D4B6DD3C3C31687 /* DeckStore.swift in Sources */, 157 | 36B5049FB042105DBA5A689C /* EditCardView.swift in Sources */, 158 | F9A9AC3406052D6F830A9220 /* EditDeckView.swift in Sources */, 159 | 1D3B8506D046FAD46CE79849 /* FlashcardsApp.swift in Sources */, 160 | BAF4FB027959D46A8FAF10BB /* SettingsView.swift in Sources */, 161 | 22EA8A37056DA9DF67942231 /* Theme.swift in Sources */, 162 | ); 163 | runOnlyForDeploymentPostprocessing = 0; 164 | }; 165 | /* End PBXSourcesBuildPhase section */ 166 | 167 | /* Begin XCBuildConfiguration section */ 168 | 0A4EF9FF4C0A56B64A1C8C34 /* Debug */ = { 169 | isa = XCBuildConfiguration; 170 | buildSettings = { 171 | ALWAYS_SEARCH_USER_PATHS = NO; 172 | CLANG_ANALYZER_NONNULL = YES; 173 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 174 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 175 | CLANG_CXX_LIBRARY = "libc++"; 176 | CLANG_ENABLE_MODULES = YES; 177 | CLANG_ENABLE_OBJC_ARC = YES; 178 | CLANG_ENABLE_OBJC_WEAK = YES; 179 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 180 | CLANG_WARN_BOOL_CONVERSION = YES; 181 | CLANG_WARN_COMMA = YES; 182 | CLANG_WARN_CONSTANT_CONVERSION = YES; 183 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INFINITE_RECURSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 192 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 193 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 194 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 195 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 196 | CLANG_WARN_STRICT_PROTOTYPES = YES; 197 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 198 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 199 | CLANG_WARN_UNREACHABLE_CODE = YES; 200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 201 | COPY_PHASE_STRIP = NO; 202 | DEBUG_INFORMATION_FORMAT = dwarf; 203 | ENABLE_STRICT_OBJC_MSGSEND = YES; 204 | ENABLE_TESTABILITY = YES; 205 | GCC_C_LANGUAGE_STANDARD = gnu11; 206 | GCC_DYNAMIC_NO_PIC = NO; 207 | GCC_NO_COMMON_BLOCKS = YES; 208 | GCC_OPTIMIZATION_LEVEL = 0; 209 | GCC_PREPROCESSOR_DEFINITIONS = ( 210 | "$(inherited)", 211 | "DEBUG=1", 212 | ); 213 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 214 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 215 | GCC_WARN_UNDECLARED_SELECTOR = YES; 216 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 217 | GCC_WARN_UNUSED_FUNCTION = YES; 218 | GCC_WARN_UNUSED_VARIABLE = YES; 219 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 220 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 221 | MTL_FAST_MATH = YES; 222 | ONLY_ACTIVE_ARCH = YES; 223 | PRODUCT_NAME = "$(TARGET_NAME)"; 224 | SDKROOT = iphoneos; 225 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 226 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 227 | SWIFT_VERSION = 5.0; 228 | }; 229 | name = Debug; 230 | }; 231 | 0A68FCEB0E7CCE9912579DC3 /* Debug */ = { 232 | isa = XCBuildConfiguration; 233 | buildSettings = { 234 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 235 | CODE_SIGN_IDENTITY = "iPhone Developer"; 236 | INFOPLIST_FILE = Flashcards/Info.plist; 237 | LD_RUNPATH_SEARCH_PATHS = ( 238 | "$(inherited)", 239 | "@executable_path/Frameworks", 240 | ); 241 | PRODUCT_BUNDLE_IDENTIFIER = com.vb.flashcards; 242 | SDKROOT = iphoneos; 243 | TARGETED_DEVICE_FAMILY = "1,2"; 244 | }; 245 | name = Debug; 246 | }; 247 | 5E972FD397138C40324CF479 /* Release */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 251 | CODE_SIGN_IDENTITY = "iPhone Developer"; 252 | INFOPLIST_FILE = Flashcards/Info.plist; 253 | LD_RUNPATH_SEARCH_PATHS = ( 254 | "$(inherited)", 255 | "@executable_path/Frameworks", 256 | ); 257 | PRODUCT_BUNDLE_IDENTIFIER = com.vb.flashcards; 258 | SDKROOT = iphoneos; 259 | TARGETED_DEVICE_FAMILY = "1,2"; 260 | }; 261 | name = Release; 262 | }; 263 | 983884999BA7C686AC85842F /* Release */ = { 264 | isa = XCBuildConfiguration; 265 | buildSettings = { 266 | ALWAYS_SEARCH_USER_PATHS = NO; 267 | CLANG_ANALYZER_NONNULL = YES; 268 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 269 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 270 | CLANG_CXX_LIBRARY = "libc++"; 271 | CLANG_ENABLE_MODULES = YES; 272 | CLANG_ENABLE_OBJC_ARC = YES; 273 | CLANG_ENABLE_OBJC_WEAK = YES; 274 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 275 | CLANG_WARN_BOOL_CONVERSION = YES; 276 | CLANG_WARN_COMMA = YES; 277 | CLANG_WARN_CONSTANT_CONVERSION = YES; 278 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 279 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 280 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 281 | CLANG_WARN_EMPTY_BODY = YES; 282 | CLANG_WARN_ENUM_CONVERSION = YES; 283 | CLANG_WARN_INFINITE_RECURSION = YES; 284 | CLANG_WARN_INT_CONVERSION = YES; 285 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 286 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 287 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 288 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 289 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 290 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 291 | CLANG_WARN_STRICT_PROTOTYPES = YES; 292 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 293 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 294 | CLANG_WARN_UNREACHABLE_CODE = YES; 295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 296 | COPY_PHASE_STRIP = NO; 297 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 298 | ENABLE_NS_ASSERTIONS = NO; 299 | ENABLE_STRICT_OBJC_MSGSEND = YES; 300 | GCC_C_LANGUAGE_STANDARD = gnu11; 301 | GCC_NO_COMMON_BLOCKS = YES; 302 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 303 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 304 | GCC_WARN_UNDECLARED_SELECTOR = YES; 305 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 306 | GCC_WARN_UNUSED_FUNCTION = YES; 307 | GCC_WARN_UNUSED_VARIABLE = YES; 308 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 309 | MTL_ENABLE_DEBUG_INFO = NO; 310 | MTL_FAST_MATH = YES; 311 | PRODUCT_NAME = "$(TARGET_NAME)"; 312 | SDKROOT = iphoneos; 313 | SWIFT_COMPILATION_MODE = wholemodule; 314 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 315 | SWIFT_VERSION = 5.0; 316 | }; 317 | name = Release; 318 | }; 319 | /* End XCBuildConfiguration section */ 320 | 321 | /* Begin XCConfigurationList section */ 322 | 486C63B2BBDB4BA36F23F6AE /* Build configuration list for PBXProject "Flashcards" */ = { 323 | isa = XCConfigurationList; 324 | buildConfigurations = ( 325 | 0A4EF9FF4C0A56B64A1C8C34 /* Debug */, 326 | 983884999BA7C686AC85842F /* Release */, 327 | ); 328 | defaultConfigurationIsVisible = 0; 329 | defaultConfigurationName = Debug; 330 | }; 331 | CE79D582715594345ADD9246 /* Build configuration list for PBXNativeTarget "Flashcards" */ = { 332 | isa = XCConfigurationList; 333 | buildConfigurations = ( 334 | 0A68FCEB0E7CCE9912579DC3 /* Debug */, 335 | 5E972FD397138C40324CF479 /* Release */, 336 | ); 337 | defaultConfigurationIsVisible = 0; 338 | defaultConfigurationName = Debug; 339 | }; 340 | /* End XCConfigurationList section */ 341 | }; 342 | rootObject = 99F96BF0A775FB829E9E9FEA /* Project object */; 343 | } 344 | --------------------------------------------------------------------------------