├── 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 |
--------------------------------------------------------------------------------