From 3ba682aa4fc23c8db614a3471c20742dff603cec Mon Sep 17 00:00:00 2001 From: Justin Oros Date: Wed, 2 Jul 2025 15:40:15 -0700 Subject: [PATCH] HAM Operator Quiz A macOS application written in Swift by Justin Oros. --- .../project.pbxproj | 326 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 48374 bytes .../xcschemes/xcschememanagement.plist | 14 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 ++++ .../Assets.xcassets/Contents.json | 6 + .../HAM Operator Quiz/FileSync.swift | 47 +++ .../HAM_Operator_Quiz.entitlements | 12 + .../HamOperatorQuizApp.swift | 13 + .../HAM Operator Quiz/QuizView.swift | 252 ++++++++++++++ .../HAM Operator Quiz/TestSelectionView.swift | 25 ++ 12 files changed, 771 insertions(+) create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.pbxproj create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/Contents.json create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/FileSync.swift create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/HAM_Operator_Quiz.entitlements create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/HamOperatorQuizApp.swift create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/QuizView.swift create mode 100644 Speedo/HAM Operator Quiz/HAM Operator Quiz/TestSelectionView.swift diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.pbxproj b/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d5a966f --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.pbxproj @@ -0,0 +1,326 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + 976BF73F2E15D180008D9B42 /* HAM Operator Quiz.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "HAM Operator Quiz.app"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 976BF7412E15D180008D9B42 /* HAM Operator Quiz */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "HAM Operator Quiz"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 976BF73C2E15D180008D9B42 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 976BF7362E15D180008D9B42 = { + isa = PBXGroup; + children = ( + 976BF7412E15D180008D9B42 /* HAM Operator Quiz */, + 976BF7402E15D180008D9B42 /* Products */, + ); + sourceTree = ""; + }; + 976BF7402E15D180008D9B42 /* Products */ = { + isa = PBXGroup; + children = ( + 976BF73F2E15D180008D9B42 /* HAM Operator Quiz.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 976BF73E2E15D180008D9B42 /* HAM Operator Quiz */ = { + isa = PBXNativeTarget; + buildConfigurationList = 976BF74B2E15D182008D9B42 /* Build configuration list for PBXNativeTarget "HAM Operator Quiz" */; + buildPhases = ( + 976BF73B2E15D180008D9B42 /* Sources */, + 976BF73C2E15D180008D9B42 /* Frameworks */, + 976BF73D2E15D180008D9B42 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 976BF7412E15D180008D9B42 /* HAM Operator Quiz */, + ); + name = "HAM Operator Quiz"; + packageProductDependencies = ( + ); + productName = "HAM Operator Quiz"; + productReference = 976BF73F2E15D180008D9B42 /* HAM Operator Quiz.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 976BF7372E15D180008D9B42 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 976BF73E2E15D180008D9B42 = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = 976BF73A2E15D180008D9B42 /* Build configuration list for PBXProject "HAM Operator Quiz" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 976BF7362E15D180008D9B42; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 976BF7402E15D180008D9B42 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 976BF73E2E15D180008D9B42 /* HAM Operator Quiz */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 976BF73D2E15D180008D9B42 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 976BF73B2E15D180008D9B42 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 976BF7492E15D182008D9B42 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 976BF74A2E15D182008D9B42 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 976BF74C2E15D182008D9B42 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "HAM Operator Quiz/HAM_Operator_Quiz.entitlements"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.justinoros.HAM-Operator-Quiz"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 976BF74D2E15D182008D9B42 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "HAM Operator Quiz/HAM_Operator_Quiz.entitlements"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.justinoros.HAM-Operator-Quiz"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 976BF73A2E15D180008D9B42 /* Build configuration list for PBXProject "HAM Operator Quiz" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 976BF7492E15D182008D9B42 /* Debug */, + 976BF74A2E15D182008D9B42 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 976BF74B2E15D182008D9B42 /* Build configuration list for PBXNativeTarget "HAM Operator Quiz" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 976BF74C2E15D182008D9B42 /* Debug */, + 976BF74D2E15D182008D9B42 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 976BF7372E15D180008D9B42 /* Project object */; +} diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate b/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..fbbfa9399e42a59091160c568cfb0a4d004dd897 GIT binary patch literal 48374 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 literal 0 HcmV?d00001 diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist b/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..1e98778 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + HAM Operator Quiz.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AccentColor.colorset/Contents.json b/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AppIcon.appiconset/Contents.json b/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/Contents.json b/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/FileSync.swift b/Speedo/HAM Operator Quiz/HAM Operator Quiz/FileSync.swift new file mode 100644 index 0000000..df943e7 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/FileSync.swift @@ -0,0 +1,47 @@ +import Foundation + +struct FileSync { + static let baseURL = "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master" + static let testPaths = [ + "technician": "technician-2022-2026/technician.json", + "general": "general-2023-2027/general.json", + "extra": "extra-2024-2028/extra.json" + ] + + static func urlForTest(_ test: String) -> URL? { + guard let path = testPaths[test.lowercased()] else { return nil } + return URL(string: "\(baseURL)/\(path)") + } + + static func localFileURL(test: String) -> URL? { + FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("\(test).json") + } + + static func downloadAndSave(test: String, completion: @escaping (Result) -> Void) { + guard + let url = urlForTest(test), + let localURL = localFileURL(test: test) + else { + completion(.failure(NSError(domain: "Invalid URL or file path", code: 0))) + return + } + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + guard let data = data else { + completion(.failure(NSError(domain: "No data", code: 0))) + return + } + do { + try data.write(to: localURL) + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + task.resume() + } +} diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/HAM_Operator_Quiz.entitlements b/Speedo/HAM Operator Quiz/HAM Operator Quiz/HAM_Operator_Quiz.entitlements new file mode 100644 index 0000000..625af03 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/HAM_Operator_Quiz.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/HamOperatorQuizApp.swift b/Speedo/HAM Operator Quiz/HAM Operator Quiz/HamOperatorQuizApp.swift new file mode 100644 index 0000000..52b09bc --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/HamOperatorQuizApp.swift @@ -0,0 +1,13 @@ +import SwiftUI + +@main +struct HamOperatorQuizApp: App { + var body: some Scene { + WindowGroup { + NavigationStack { + TestSelectionView() + } + .frame(minWidth: 300, minHeight: 200) + } + } +} diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/QuizView.swift b/Speedo/HAM Operator Quiz/HAM Operator Quiz/QuizView.swift new file mode 100644 index 0000000..c0f1977 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/QuizView.swift @@ -0,0 +1,252 @@ +import SwiftUI + +struct QuizQuestion: Codable, Identifiable { + let id: String + let question: String + let correct: Int + let answers: [String] +} + +struct QuizView: View { + let testName: String + + @State private var questions: [QuizQuestion] = [] + @State private var currentIndex = 0 + @State private var selectedAnswer: String? = nil + @State private var score = 0 + @State private var feedbackText = "" + @State private var feedbackColor = Color.primary + @State private var loadError = false + @State private var showResults = false + @State private var autoAdvanceWorkItem: DispatchWorkItem? = nil + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + if loadError { + Text("Could not load questions for \(testName).") + .foregroundColor(.red) + .padding() + } else if questions.isEmpty { + Text("Loading questions...") + .padding() + } else if currentIndex < questions.count { + Text(questions[currentIndex].question) + .font(.headline) + .fixedSize(horizontal: false, vertical: true) + + ForEach(questions[currentIndex].answers, id: \.self) { answer in + Button(action: { + selectedAnswer = answer + checkAnswer() + scheduleAutoNext() + }) { + Text(answer) + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + Capsule() + .fill(bubbleColor(for: answer)) + ) + .foregroundColor(.white) + } + .buttonStyle(PlainButtonStyle()) + .disabled(selectedAnswer != nil) // Disable after selection + } + + Text(feedbackText) + .foregroundColor(feedbackColor) + + HStack { + Button("Next") { + cancelAutoNext() + nextQuestion() + } + .disabled(selectedAnswer == nil) + + Button("Quit") { + cancelAutoNext() + showFinalResultsAndExit() + } + } + } else { + VStack(spacing: 10) { + Text("Quiz Complete!") + .font(.title) + + let percentage = questions.count > 0 ? Int((Double(score) / Double(questions.count)) * 100) : 0 + Text("You got \(score) out of \(questions.count) correct. (\(percentage)%)") + + Button("Quit") { + NSApp.terminate(nil) + } + } + } + } + .padding() + .frame(minWidth: 600, minHeight: 400) + .onAppear(perform: syncAndLoadQuestions) + .navigationTitle(testName + " Exam") + } + + func bubbleColor(for answer: String) -> Color { + let imessageBlue = Color(red: 0.0, green: 122/255, blue: 1.0) + let correctAnswer = questions[currentIndex].answers[questions[currentIndex].correct] + + if let selected = selectedAnswer { + if selected == answer && selected == correctAnswer { + return .green + } else if selected == answer && selected != correctAnswer { + // Selected wrong answer -> red bubble + return .red + } else if answer == correctAnswer && selected != correctAnswer { + // Highlight correct answer bubble green if selected answer is wrong + return .green + } + } + return imessageBlue + } + + func showFinalResultsAndExit() { + let percentage = currentIndex > 0 ? Int((Double(score) / Double(currentIndex + 1)) * 100) : 0 + feedbackText = "You answered \(score) out of \(currentIndex + 1) correctly. (\(percentage)% correct)" + feedbackColor = .primary + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + NSApp.terminate(nil) + } + } + + func scheduleAutoNext() { + cancelAutoNext() // Cancel any existing auto advance + autoAdvanceWorkItem = DispatchWorkItem { + if selectedAnswer != nil && currentIndex < questions.count - 1 { + nextQuestion() + } + } + if let workItem = autoAdvanceWorkItem { + DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: workItem) + } + } + + func cancelAutoNext() { + autoAdvanceWorkItem?.cancel() + autoAdvanceWorkItem = nil + } + + func syncAndLoadQuestions() { + guard let documentsDir = getDocumentsDirectory() else { + loadError = true + return + } + + let lowerName = testName.lowercased() + let filename = "\(lowerName).json" + let urlMap = [ + "technician": "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master/technician-2022-2026/technician.json", + "general": "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master/general-2023-2027/general.json", + "extra": "https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/master/extra-2024-2028/extra.json" + ] + + guard let remoteUrlString = urlMap[lowerName], + let remoteUrl = URL(string: remoteUrlString) else { + loadError = true + return + } + + let localFileURL = documentsDir.appendingPathComponent(filename) + + let fileExists = FileManager.default.fileExists(atPath: localFileURL.path) + + if !fileExists { + downloadAndSave(remoteUrl: remoteUrl, to: localFileURL) + } else { + URLSession.shared.dataTask(with: remoteUrl) { data, response, error in + if let data = data, + let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 { + do { + let remoteQuestions = try JSONDecoder().decode([QuizQuestion].self, from: data) + if let localData = try? Data(contentsOf: localFileURL), + let localQuestions = try? JSONDecoder().decode([QuizQuestion].self, from: localData), + localQuestions.count == remoteQuestions.count { + loadQuestions(from: localFileURL) + } else { + try data.write(to: localFileURL) + loadQuestions(from: localFileURL) + } + } catch { + print("Failed sync check: \(error)") + loadQuestions(from: localFileURL) + } + } else { + print("Remote check failed: \(error?.localizedDescription ?? "Unknown")") + loadQuestions(from: localFileURL) + } + }.resume() + } + } + + func downloadAndSave(remoteUrl: URL, to localUrl: URL) { + URLSession.shared.dataTask(with: remoteUrl) { data, response, error in + if let data = data, + let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 { + do { + try data.write(to: localUrl) + loadQuestions(from: localUrl) + } catch { + print("Failed to save downloaded JSON: \(error)") + loadError = true + } + } else { + print("Download failed: \(error?.localizedDescription ?? "Unknown")") + loadError = true + } + }.resume() + } + + func loadQuestions(from fileURL: URL) { + DispatchQueue.global(qos: .background).async { + do { + let data = try Data(contentsOf: fileURL) + let loadedQuestions = try JSONDecoder().decode([QuizQuestion].self, from: data) + + DispatchQueue.main.async { + if loadedQuestions.isEmpty { + loadError = true + } else { + questions = loadedQuestions.shuffled() + loadError = false + } + } + } catch { + print("Failed to parse questions: \(error)") + DispatchQueue.main.async { + loadError = true + } + } + } + } + + func checkAnswer() { + let correctAnswer = questions[currentIndex].answers[questions[currentIndex].correct] + if selectedAnswer == correctAnswer { + score += 1 + feedbackText = "Correct!" + feedbackColor = .green + } else { + feedbackText = "Incorrect! Correct answer: \(correctAnswer)" + feedbackColor = .red + } + } + + func nextQuestion() { + cancelAutoNext() + currentIndex += 1 + selectedAnswer = nil + feedbackText = "" + } + + func getDocumentsDirectory() -> URL? { + FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + } +} diff --git a/Speedo/HAM Operator Quiz/HAM Operator Quiz/TestSelectionView.swift b/Speedo/HAM Operator Quiz/HAM Operator Quiz/TestSelectionView.swift new file mode 100644 index 0000000..a6af407 --- /dev/null +++ b/Speedo/HAM Operator Quiz/HAM Operator Quiz/TestSelectionView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct TestSelectionView: View { + let testOptions = ["Technician Class", "General Class", "Extra Class"] + + var body: some View { + VStack { + List(testOptions, id: \.self) { test in + NavigationLink(value: test) { + Text(test) + } + } + .navigationTitle("Select Test") + .navigationDestination(for: String.self) { selectedTest in + QuizView(testName: selectedTest.replacingOccurrences(of: " Class", with: "")) + } + .frame(minWidth: 400, minHeight: 300) + + Button("Quit") { + NSApp.terminate(nil) + } + .padding(.bottom, 10) + } + } +}