diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ed7978c33a --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# CocoaPods +Pods/ + +# Xcode pessoal e temporários +.DS_Store +DerivedData/ +*.xcuserstate \ No newline at end of file diff --git a/DynamoxQuiz.xcodeproj/project.pbxproj b/DynamoxQuiz.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..46728c4aab --- /dev/null +++ b/DynamoxQuiz.xcodeproj/project.pbxproj @@ -0,0 +1,618 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 57BC3EFAD3283908327E2450 /* libPods-DynamoxQuizTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 154B3BFC016E9A672F7D9814 /* libPods-DynamoxQuizTests.a */; }; + 7C0A0B81BEBAA9DBA9D25C7C /* Pods_DynamoxQuiz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11564E6F2A9760EECFDCB64A /* Pods_DynamoxQuiz.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 272A2A7F2F5BB75600DE4718 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2790FE4B2F562569002E9F05 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2790FE522F562569002E9F05; + remoteInfo = DynamoxQuiz; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 11564E6F2A9760EECFDCB64A /* Pods_DynamoxQuiz.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DynamoxQuiz.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 154B3BFC016E9A672F7D9814 /* libPods-DynamoxQuizTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DynamoxQuizTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 272A2A7B2F5BB75600DE4718 /* DynamoxQuizTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DynamoxQuizTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2790FE532F562569002E9F05 /* DynamoxQuiz.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamoxQuiz.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 62EB79803D778B0A54A43513 /* Pods-DynamoxQuiz.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DynamoxQuiz.release.xcconfig"; path = "Target Support Files/Pods-DynamoxQuiz/Pods-DynamoxQuiz.release.xcconfig"; sourceTree = ""; }; + 924576AAB79C455316F01EEE /* Pods-DynamoxQuizTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DynamoxQuizTests.release.xcconfig"; path = "Target Support Files/Pods-DynamoxQuizTests/Pods-DynamoxQuizTests.release.xcconfig"; sourceTree = ""; }; + F5AB9A1CABBB52258949D7DC /* Pods-DynamoxQuiz.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DynamoxQuiz.debug.xcconfig"; path = "Target Support Files/Pods-DynamoxQuiz/Pods-DynamoxQuiz.debug.xcconfig"; sourceTree = ""; }; + F6CDE76DAE9616784E08AF42 /* Pods-DynamoxQuizTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DynamoxQuizTests.debug.xcconfig"; path = "Target Support Files/Pods-DynamoxQuizTests/Pods-DynamoxQuizTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 2790FE652F56256C002E9F05 /* Exceptions for "DynamoxQuiz" folder in "DynamoxQuiz" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 2790FE522F562569002E9F05 /* DynamoxQuiz */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 272A2A7C2F5BB75600DE4718 /* DynamoxQuizTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DynamoxQuizTests; + sourceTree = ""; + }; + 2790FE552F562569002E9F05 /* DynamoxQuiz */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2790FE652F56256C002E9F05 /* Exceptions for "DynamoxQuiz" folder in "DynamoxQuiz" target */, + ); + path = DynamoxQuiz; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 272A2A782F5BB75600DE4718 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 57BC3EFAD3283908327E2450 /* libPods-DynamoxQuizTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2790FE502F562569002E9F05 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C0A0B81BEBAA9DBA9D25C7C /* Pods_DynamoxQuiz.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2790FE4A2F562569002E9F05 = { + isa = PBXGroup; + children = ( + 2790FE552F562569002E9F05 /* DynamoxQuiz */, + 272A2A7C2F5BB75600DE4718 /* DynamoxQuizTests */, + 2790FE542F562569002E9F05 /* Products */, + A71BB488142E4BD5AFB01730 /* Pods */, + 4D9F5B42D06C00C6BFCFFE05 /* Frameworks */, + ); + sourceTree = ""; + }; + 2790FE542F562569002E9F05 /* Products */ = { + isa = PBXGroup; + children = ( + 2790FE532F562569002E9F05 /* DynamoxQuiz.app */, + 272A2A7B2F5BB75600DE4718 /* DynamoxQuizTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 4D9F5B42D06C00C6BFCFFE05 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 11564E6F2A9760EECFDCB64A /* Pods_DynamoxQuiz.framework */, + 154B3BFC016E9A672F7D9814 /* libPods-DynamoxQuizTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + A71BB488142E4BD5AFB01730 /* Pods */ = { + isa = PBXGroup; + children = ( + F5AB9A1CABBB52258949D7DC /* Pods-DynamoxQuiz.debug.xcconfig */, + 62EB79803D778B0A54A43513 /* Pods-DynamoxQuiz.release.xcconfig */, + F6CDE76DAE9616784E08AF42 /* Pods-DynamoxQuizTests.debug.xcconfig */, + 924576AAB79C455316F01EEE /* Pods-DynamoxQuizTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 272A2A7A2F5BB75600DE4718 /* DynamoxQuizTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 272A2A812F5BB75600DE4718 /* Build configuration list for PBXNativeTarget "DynamoxQuizTests" */; + buildPhases = ( + EBE25CCB6C86225FA2A08A37 /* [CP] Check Pods Manifest.lock */, + 272A2A772F5BB75600DE4718 /* Sources */, + 272A2A782F5BB75600DE4718 /* Frameworks */, + 272A2A792F5BB75600DE4718 /* Resources */, + 2D5EE63B55AE118913913A47 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 272A2A802F5BB75600DE4718 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 272A2A7C2F5BB75600DE4718 /* DynamoxQuizTests */, + ); + name = DynamoxQuizTests; + productName = DynamoxQuizTests; + productReference = 272A2A7B2F5BB75600DE4718 /* DynamoxQuizTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2790FE522F562569002E9F05 /* DynamoxQuiz */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2790FE662F56256C002E9F05 /* Build configuration list for PBXNativeTarget "DynamoxQuiz" */; + buildPhases = ( + 564658F36F40B962FF5B5228 /* [CP] Check Pods Manifest.lock */, + 2790FE4F2F562569002E9F05 /* Sources */, + 2790FE502F562569002E9F05 /* Frameworks */, + 2790FE512F562569002E9F05 /* Resources */, + FD9076A2C1C7727D9C254ED3 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2790FE552F562569002E9F05 /* DynamoxQuiz */, + ); + name = DynamoxQuiz; + productName = DynamoxQuiz; + productReference = 2790FE532F562569002E9F05 /* DynamoxQuiz.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2790FE4B2F562569002E9F05 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2630; + LastUpgradeCheck = 2630; + TargetAttributes = { + 272A2A7A2F5BB75600DE4718 = { + CreatedOnToolsVersion = 26.3; + TestTargetID = 2790FE522F562569002E9F05; + }; + 2790FE522F562569002E9F05 = { + CreatedOnToolsVersion = 26.3; + }; + }; + }; + buildConfigurationList = 2790FE4E2F562569002E9F05 /* Build configuration list for PBXProject "DynamoxQuiz" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2790FE4A2F562569002E9F05; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 2790FE542F562569002E9F05 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2790FE522F562569002E9F05 /* DynamoxQuiz */, + 272A2A7A2F5BB75600DE4718 /* DynamoxQuizTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 272A2A792F5BB75600DE4718 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2790FE512F562569002E9F05 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2D5EE63B55AE118913913A47 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DynamoxQuizTests/Pods-DynamoxQuizTests-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DynamoxQuizTests/Pods-DynamoxQuizTests-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DynamoxQuizTests/Pods-DynamoxQuizTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 564658F36F40B962FF5B5228 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-DynamoxQuiz-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EBE25CCB6C86225FA2A08A37 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-DynamoxQuizTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FD9076A2C1C7727D9C254ED3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DynamoxQuiz/Pods-DynamoxQuiz-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DynamoxQuiz/Pods-DynamoxQuiz-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DynamoxQuiz/Pods-DynamoxQuiz-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 272A2A772F5BB75600DE4718 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2790FE4F2F562569002E9F05 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 272A2A802F5BB75600DE4718 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2790FE522F562569002E9F05 /* DynamoxQuiz */; + targetProxy = 272A2A7F2F5BB75600DE4718 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 272A2A822F5BB75600DE4718 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F6CDE76DAE9616784E08AF42 /* Pods-DynamoxQuizTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9979BN6G3A; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Mateus.DynamoxQuizTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DynamoxQuiz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DynamoxQuiz"; + }; + name = Debug; + }; + 272A2A832F5BB75600DE4718 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 924576AAB79C455316F01EEE /* Pods-DynamoxQuizTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9979BN6G3A; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Mateus.DynamoxQuizTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DynamoxQuiz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DynamoxQuiz"; + }; + name = Release; + }; + 2790FE672F56256C002E9F05 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F5AB9A1CABBB52258949D7DC /* Pods-DynamoxQuiz.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9979BN6G3A; + EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DynamoxQuiz/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Mateus.DynamoxQuiz; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2790FE682F56256C002E9F05 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 62EB79803D778B0A54A43513 /* Pods-DynamoxQuiz.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 9979BN6G3A; + EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DynamoxQuiz/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Mateus.DynamoxQuiz; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2790FE692F56256C002E9F05 /* 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 = 9979BN6G3A; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2790FE6A2F56256C002E9F05 /* 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 = 9979BN6G3A; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 272A2A812F5BB75600DE4718 /* Build configuration list for PBXNativeTarget "DynamoxQuizTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 272A2A822F5BB75600DE4718 /* Debug */, + 272A2A832F5BB75600DE4718 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2790FE4E2F562569002E9F05 /* Build configuration list for PBXProject "DynamoxQuiz" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2790FE692F56256C002E9F05 /* Debug */, + 2790FE6A2F56256C002E9F05 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2790FE662F56256C002E9F05 /* Build configuration list for PBXNativeTarget "DynamoxQuiz" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2790FE672F56256C002E9F05 /* Debug */, + 2790FE682F56256C002E9F05 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2790FE4B2F562569002E9F05 /* Project object */; +} diff --git a/DynamoxQuiz.xcodeproj/xcuserdata/mateus.xcuserdatad/xcschemes/xcschememanagement.plist b/DynamoxQuiz.xcodeproj/xcuserdata/mateus.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..f5943728c1 --- /dev/null +++ b/DynamoxQuiz.xcodeproj/xcuserdata/mateus.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + DynamoxQuiz.xcscheme_^#shared#^_ + + orderHint + 6 + + + + diff --git a/DynamoxQuiz.xcworkspace/contents.xcworkspacedata b/DynamoxQuiz.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..b698cd5b22 --- /dev/null +++ b/DynamoxQuiz.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/DynamoxQuiz.xcworkspace/xcuserdata/mateus.xcuserdatad/IDEFindNavigatorScopes.plist b/DynamoxQuiz.xcworkspace/xcuserdata/mateus.xcuserdatad/IDEFindNavigatorScopes.plist new file mode 100644 index 0000000000..5dd5da85fd --- /dev/null +++ b/DynamoxQuiz.xcworkspace/xcuserdata/mateus.xcuserdatad/IDEFindNavigatorScopes.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/DynamoxQuiz/App/AppDelegate.swift b/DynamoxQuiz/App/AppDelegate.swift new file mode 100644 index 0000000000..93d14bd1cd --- /dev/null +++ b/DynamoxQuiz/App/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/DynamoxQuiz/App/SceneDelegate.swift b/DynamoxQuiz/App/SceneDelegate.swift new file mode 100644 index 0000000000..22bd1647e6 --- /dev/null +++ b/DynamoxQuiz/App/SceneDelegate.swift @@ -0,0 +1,68 @@ +// +// SceneDelegate.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + let profileViewModel = ProfileViewModel() + var flowController: AppCoordinator? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: windowScene) + self.window = window + + window.rootViewController = SplashViewController() + window.makeKeyAndVisible() + + // SceneDelegate + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + self.flowController = AppCoordinator(profileViewModel: self.profileViewModel) + + if let navController = self.flowController?.start() { + UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: { + window.rootViewController = navController + }) + } + } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/DynamoxQuiz/Assets.xcassets/AccentColor.colorset/Contents.json b/DynamoxQuiz/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..c7f31bb62e --- /dev/null +++ b/DynamoxQuiz/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,27 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.659", + "green" : "0.780", + "red" : "0.098" + } + }, + "idiom" : "universal" + }, + { + "idiom" : "universal", + "locale" : "en" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} diff --git a/DynamoxQuiz/Assets.xcassets/AppIcon.appiconset/Contents.json b/DynamoxQuiz/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..500b2fe923 --- /dev/null +++ b/DynamoxQuiz/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Design sem nome-8.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DynamoxQuiz/Assets.xcassets/AppIcon.appiconset/Design sem nome-8.png b/DynamoxQuiz/Assets.xcassets/AppIcon.appiconset/Design sem nome-8.png new file mode 100644 index 0000000000..31dda40ae1 Binary files /dev/null and b/DynamoxQuiz/Assets.xcassets/AppIcon.appiconset/Design sem nome-8.png differ diff --git a/DynamoxQuiz/Assets.xcassets/Contents.json b/DynamoxQuiz/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DynamoxQuiz/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DynamoxQuiz/Assets.xcassets/LogoSplash.imageset/Contents.json b/DynamoxQuiz/Assets.xcassets/LogoSplash.imageset/Contents.json new file mode 100644 index 0000000000..75219a8973 --- /dev/null +++ b/DynamoxQuiz/Assets.xcassets/LogoSplash.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Design sem nome-8.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DynamoxQuiz/Assets.xcassets/LogoSplash.imageset/Design sem nome-8.png b/DynamoxQuiz/Assets.xcassets/LogoSplash.imageset/Design sem nome-8.png new file mode 100644 index 0000000000..31dda40ae1 Binary files /dev/null and b/DynamoxQuiz/Assets.xcassets/LogoSplash.imageset/Design sem nome-8.png differ diff --git a/DynamoxQuiz/Components/Button/ButtonViewComponent.swift b/DynamoxQuiz/Components/Button/ButtonViewComponent.swift new file mode 100644 index 0000000000..c9fb59bc20 --- /dev/null +++ b/DynamoxQuiz/Components/Button/ButtonViewComponent.swift @@ -0,0 +1,39 @@ +// +// ButtonViewComponent.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class ButtonViewComponent: UIButton { + + private var action: (() -> Void)? + + init(title: String){ + super.init(frame: .zero) + + setTitle(title, for: .normal) + setTitleColor(.white, for: .normal) + titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) + backgroundColor = Colors.primaryGreenBase + layer.cornerRadius = 6 + translatesAutoresizingMaskIntoConstraints = false + + addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + } + + func setAction(_ action: @escaping () -> Void){ + self.action = action + } + + + @objc private func buttonTapped() { + action?() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/DynamoxQuiz/Components/Cards/CardViewComponent.swift b/DynamoxQuiz/Components/Cards/CardViewComponent.swift new file mode 100644 index 0000000000..cb07e7daf8 --- /dev/null +++ b/DynamoxQuiz/Components/Cards/CardViewComponent.swift @@ -0,0 +1,35 @@ +// +// CardViewComponent.swift +// DynamoxQuiz +// +// Created by Mateus on 05/03/26. +// + +import SwiftUI + +struct CardViewComponent: View { + + let title: String + let value: String + + var body: some View { + HStack { + VStack { + Text(title) + .font(.headline) + .bold() + Text(value) + .font(.subheadline) + } + .frame(width: 100, height: 80) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(Colors.primaryGreenBase), lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + } + .padding(.horizontal, 8) + } +} diff --git a/DynamoxQuiz/Components/Cards/CardViewHistoryComponent.swift b/DynamoxQuiz/Components/Cards/CardViewHistoryComponent.swift new file mode 100644 index 0000000000..416a6c98e1 --- /dev/null +++ b/DynamoxQuiz/Components/Cards/CardViewHistoryComponent.swift @@ -0,0 +1,70 @@ +// +// CardViewHistoryComponent.swift +// DynamoxQuiz +// +// Created by Mateus on 05/03/26. +// + +import SwiftUI + +struct CardViewHistoryComponent: View { + + let icon: String + let title: String + let date: String + let store: String + + var body: some View { + + HStack(spacing: 12) { + + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(width: 34, height: 34) + .padding(12) + .foregroundStyle(Color.white) + .background(Color(Colors.primaryGreenBase)) + .cornerRadius(.infinity) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.subheadline) + .bold() + Text(date) + .font(.caption) + .foregroundStyle(.gray) + } + + Spacer() + + VStack(spacing: 2) { + + Text(store) + .font(.title3) + .bold() + .foregroundStyle(Color(Colors.primaryGreenBase)) + + Text("acerto") + .font(.caption2) + .foregroundStyle(.gray) + } + + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color(Colors.primaryGreenBase).opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + } + .padding() + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(Colors.primaryGreenBase).opacity(0.5), lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + .padding(.horizontal) + + } +} diff --git a/DynamoxQuiz/Components/Profile/ProfileImageComponent.swift b/DynamoxQuiz/Components/Profile/ProfileImageComponent.swift new file mode 100644 index 0000000000..df2e563735 --- /dev/null +++ b/DynamoxQuiz/Components/Profile/ProfileImageComponent.swift @@ -0,0 +1,33 @@ +// +// ProfileImageComponent.swift +// DynamoxQuiz +// +// Created by Mateus on 07/03/26. +// + +import SwiftUI + +struct ProfileImageView: View { + let image: UIImage? + + var body: some View { + if let image { + Image(uiImage: image) + .resizable() + .scaledToFill() + .frame(width: 120, height: 120) + .clipShape(Circle()) + .overlay( + Circle() + .stroke(Color(Colors.primaryGreenBase), lineWidth: 3) + ) + } else { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFill() + .frame(width: 120, height: 120) + .clipShape(Circle()) + .foregroundStyle(Color(Colors.primaryGreenBase)) + } + } +} diff --git a/DynamoxQuiz/Components/QuestionsCard/QuestionCardViewComponent.swift b/DynamoxQuiz/Components/QuestionsCard/QuestionCardViewComponent.swift new file mode 100644 index 0000000000..64f681d748 --- /dev/null +++ b/DynamoxQuiz/Components/QuestionsCard/QuestionCardViewComponent.swift @@ -0,0 +1,103 @@ +// +// QuestionCardComponent.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class QuestionCardComponent: UIView { + + var onSelect: (() -> Void)? + var isSelected: Bool = false + + + private lazy var btnResult: UIButton = { + let btnRes = UIButton(type: .system) + btnRes.backgroundColor = .white + btnRes.layer.cornerRadius = 12 + btnRes.translatesAutoresizingMaskIntoConstraints = false + + btnRes.layer.shadowColor = UIColor.black.cgColor + btnRes.layer.shadowOpacity = 0.15 + btnRes.layer.shadowOffset = CGSize(width: 0, height: 4) + btnRes.layer.shadowRadius = 6 + + btnRes.addTarget(self, action: #selector(handleBtnClick), for: .touchUpInside) + return btnRes + }() + + private let textLabel: UILabel = { + let label = UILabel() + + label.font = .systemFont(ofSize: 18, weight: .semibold) + label.textColor = .black + + + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.textAlignment = .left + + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + init(title: String){ + super.init(frame: .zero) + textLabel.text = title + + setupUI() + setupConstraints() + + translatesAutoresizingMaskIntoConstraints = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupSelfClass(){ + layer.cornerRadius = 10 + translatesAutoresizingMaskIntoConstraints = false + } + + private func setupUI(){ + backgroundColor = .white + + addSubview(btnResult) + btnResult.addSubview(textLabel) + + } + + private func setupConstraints(){ + NSLayoutConstraint.activate([ + btnResult.topAnchor.constraint(equalTo: topAnchor), + btnResult.leadingAnchor.constraint(equalTo: leadingAnchor), + btnResult.trailingAnchor.constraint(equalTo: trailingAnchor), + btnResult.bottomAnchor.constraint(equalTo: bottomAnchor), + + textLabel.topAnchor.constraint(equalTo: btnResult.topAnchor, constant: 15), + textLabel.leadingAnchor.constraint(equalTo: btnResult.leadingAnchor, constant: 20), + textLabel.trailingAnchor.constraint(equalTo: btnResult.trailingAnchor, constant: -20), + textLabel.bottomAnchor.constraint(equalTo: btnResult.bottomAnchor, constant: -15) + ]) + } + @objc private func handleBtnClick() { + onSelect?() + print("cliquei -> isChecked is true") + } + + func setSelection(_ isSelected: Bool) { + self.isSelected = isSelected + + btnResult.layer.borderWidth = isSelected ? 1.0 : 0 + btnResult.layer.borderColor = isSelected ? Colors.primaryGreenBase.cgColor : nil + } + func configure(with text: String){ + textLabel.text = text + } + func getAnswerText() -> String? { + return self.textLabel.text + } +} diff --git a/DynamoxQuiz/Components/Toast/ToastMesageComponent.swift b/DynamoxQuiz/Components/Toast/ToastMesageComponent.swift new file mode 100644 index 0000000000..7131492cb8 --- /dev/null +++ b/DynamoxQuiz/Components/Toast/ToastMesageComponent.swift @@ -0,0 +1,49 @@ +// +// ToastMesageComponent.swift +// DynamoxQuiz +// +// Created by Mateus on 08/03/26. +// + +import SwiftUI +import UIKit + +struct ToastMessageComponent: View { + let message: String + + var body: some View { + Text(message) + .foregroundStyle(.white) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color(Colors.primaryGreenBase)) + .cornerRadius(8) + } + + static func show(message: String, in viewController: UIViewController) { + let toast = UIHostingController(rootView: ToastMessageComponent(message: message)) + toast.view.backgroundColor = .clear + toast.view.translatesAutoresizingMaskIntoConstraints = false + + viewController.addChild(toast) + viewController.view.addSubview(toast.view) + toast.didMove(toParent: viewController) + + NSLayoutConstraint.activate([ + toast.view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor), + toast.view.bottomAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.bottomAnchor, constant: -82) + ]) + + toast.view.alpha = 0 + UIView.animate(withDuration: 0.3) { toast.view.alpha = 1 } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + UIView.animate(withDuration: 0.3, animations: { + toast.view.alpha = 0 + }) { _ in + toast.view.removeFromSuperview() + toast.removeFromParent() + } + } + } +} diff --git a/DynamoxQuiz/Coordinators/AppCoordinator.swift b/DynamoxQuiz/Coordinators/AppCoordinator.swift new file mode 100644 index 0000000000..93f3e341c2 --- /dev/null +++ b/DynamoxQuiz/Coordinators/AppCoordinator.swift @@ -0,0 +1,51 @@ +// +// DynamoxFlowController.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import Foundation +import UIKit + +class AppCoordinator { + private let profileViewModel: ProfileViewModel + private var navigationController: UINavigationController? + + public init(profileViewModel: ProfileViewModel){ + self.profileViewModel = profileViewModel + } + + func start() -> UIViewController?{ + if profileViewModel.hasUser { + return MainTabBarController(profileViewModel: profileViewModel) + } else { + let nav = UINavigationController(rootViewController: + HomeViewController(flowDelegate: self, profileViewModel: profileViewModel)) + return nav + } + } +} + +// MARK: - Splash + +extension AppCoordinator: SplashFlowDelegate { + func navigateToHome() { + let homeVC = HomeViewController(flowDelegate: self, profileViewModel: profileViewModel) + navigationController?.setViewControllers([homeVC], animated: true) + } +} + + +// MARK: Home + +extension AppCoordinator: HomeFlowDelegate { + func navigateToQuiz() { + if let window = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first?.windows.first { + window.rootViewController = MainTabBarController(profileViewModel: profileViewModel) + window.makeKeyAndVisible() + } + } +} diff --git a/DynamoxQuiz/Core/Constants/Colors/Colors-Constants.swift b/DynamoxQuiz/Core/Constants/Colors/Colors-Constants.swift new file mode 100644 index 0000000000..edf38d55f9 --- /dev/null +++ b/DynamoxQuiz/Core/Constants/Colors/Colors-Constants.swift @@ -0,0 +1,24 @@ +// +// Colors-Constants.swift +// DynamoxQuiz +// +// Created by Mateus on 04/03/26. +// + +import Foundation +import UIKit + +public struct Colors { + nonisolated static let primaryGreenBase = UIColor( + red: 25/255, + green: 199/255, + blue: 168/255, + alpha: 1.0 + ) + nonisolated static let lightGrayBase = UIColor( + red: 243/255, + green: 243/255, + blue: 243/255, + alpha: 1 + ) +} diff --git a/DynamoxQuiz/Core/Network/APIService.swift b/DynamoxQuiz/Core/Network/APIService.swift new file mode 100644 index 0000000000..7049da6578 --- /dev/null +++ b/DynamoxQuiz/Core/Network/APIService.swift @@ -0,0 +1,45 @@ +// +// APIService.swift +// DynamoxQuiz +// +// Created by Mateus on 03/03/26. +// + +import Foundation +import Alamofire + +struct APIService: APIServiceDelegate { + private let baseURL = "https://quiz-api-bwi5hjqyaq-uc.a.run.app" + + nonisolated func fetchRandomQuestion() async throws -> Question { + let url = "\(baseURL)/question" +// let url = String(format: "%@/question", baseURL) + + return try await AF.request(url) + .validate() + .serializingDecodable(Question.self) + .value + } + + nonisolated func validateAnswer(questionId: String, answer: String) async throws -> Bool { + let url = "\(baseURL)/answer?questionId=\(questionId)" + + let parameters: [String: String] = [ + "questionId": questionId, + "answer": answer + ] + + let response = try await AF.request( + url, + method: .post, + parameters: parameters, + encoder: JSONParameterEncoder.default + ) + .validate() + .serializingDecodable(AnswerResponse.self) + .value + + return response.result + } +} + diff --git a/DynamoxQuiz/Core/Network/APIServiceDelegate.swift b/DynamoxQuiz/Core/Network/APIServiceDelegate.swift new file mode 100644 index 0000000000..4d5e4f40c8 --- /dev/null +++ b/DynamoxQuiz/Core/Network/APIServiceDelegate.swift @@ -0,0 +1,11 @@ +// +// APIServiceDelegate.swift +// DynamoxQuiz +// +// Created by Mateus on 07/03/26. +// + +protocol APIServiceDelegate { + func fetchRandomQuestion() async throws -> Question + func validateAnswer(questionId: String, answer: String) async throws -> Bool +} diff --git a/DynamoxQuiz/Core/Persistence/CoreDataStack.swift b/DynamoxQuiz/Core/Persistence/CoreDataStack.swift new file mode 100644 index 0000000000..34b01ee2f8 --- /dev/null +++ b/DynamoxQuiz/Core/Persistence/CoreDataStack.swift @@ -0,0 +1,27 @@ +// +// CoreDataStack.swift +// DynamoxQuiz +// +// Created by Mateus on 05/03/26. +// + +import CoreData + +class CoreDataStack { + static let shared = CoreDataStack() + + lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "QuizEntity") + + container.loadPersistentStores { _, error in + if let error = error { + fatalError("Erro ao carregar o Core Data: \(error)") + } + } + return container + }() + + var context : NSManagedObjectContext { + return persistentContainer.viewContext + } +} diff --git a/DynamoxQuiz/Core/Persistence/QuizEntity.xcdatamodel/contents b/DynamoxQuiz/Core/Persistence/QuizEntity.xcdatamodel/contents new file mode 100644 index 0000000000..20c53ff45e --- /dev/null +++ b/DynamoxQuiz/Core/Persistence/QuizEntity.xcdatamodel/contents @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DynamoxQuiz/Core/Persistence/QuizRepository.swift b/DynamoxQuiz/Core/Persistence/QuizRepository.swift new file mode 100644 index 0000000000..1b3aab4a40 --- /dev/null +++ b/DynamoxQuiz/Core/Persistence/QuizRepository.swift @@ -0,0 +1,48 @@ +// +// QuizRepository.swift +// DynamoxQuiz +// +// Created by Mateus on 05/03/26. +// + +import CoreData + +class QuizRepository { + + private let context = CoreDataStack.shared.context + + func create(name: String, correct: Int16, total: Int16, rounds: Int32){ + let match = NSEntityDescription.insertNewObject( + forEntityName: "HistoryQuiz", + into: context + ) + + match.setValue(correct, forKey: "correctAnswers") + match.setValue(total, forKey: "totalQuestions") + match.setValue(rounds, forKey: "totalRounds") + match.setValue(Date(), forKey: "date") + + save() + } + + func search() -> [NSManagedObject] { + let request = NSFetchRequest(entityName: "HistoryQuiz") + request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] + + do { + return try context.fetch(request) + } catch { + print("Erro ao salvar. \(error)") + return [] + } + } + public func save() { + guard context.hasChanges else { return } + do { + try context.save() + } catch { + print("Erro ao salvar: \(error)") + } + } + +} diff --git a/DynamoxQuiz/Core/Persistence/UserRepository.swift b/DynamoxQuiz/Core/Persistence/UserRepository.swift new file mode 100644 index 0000000000..182404d537 --- /dev/null +++ b/DynamoxQuiz/Core/Persistence/UserRepository.swift @@ -0,0 +1,30 @@ +// +// UserRepository.swift +// DynamoxQuiz +// +// Created by Mateus on 07/03/26. +// +import CoreData + +class UserRepository { + private let context = CoreDataStack.shared.context + + func saveUser(name: String, imageData: Data?) { + let user = fetchUser() ?? NSEntityDescription.insertNewObject( + forEntityName: "User", + into: context + ) + user.setValue(name, forKey: "name") + user.setValue(imageData, forKey: "profileImage") + save() + } + func fetchUser() -> NSManagedObject? { + let request = NSFetchRequest(entityName: "User") + return try? context.fetch(request).first + } + + private func save() { + guard context.hasChanges else { return } + try? context.save() + } +} diff --git a/DynamoxQuiz/Info.plist b/DynamoxQuiz/Info.plist new file mode 100644 index 0000000000..0eb786dc15 --- /dev/null +++ b/DynamoxQuiz/Info.plist @@ -0,0 +1,23 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/DynamoxQuiz/Models/QuizQuestions.swift b/DynamoxQuiz/Models/QuizQuestions.swift new file mode 100644 index 0000000000..0fd827e789 --- /dev/null +++ b/DynamoxQuiz/Models/QuizQuestions.swift @@ -0,0 +1,19 @@ +// +// QuestionModels.swift +// DynamoxQuiz +// +// Created by Mateus on 03/03/26. +// +import Foundation + +nonisolated struct Question: Decodable, Sendable { + let id: String + let statement: String + let options: [String] + + +} + +nonisolated struct AnswerResponse: Decodable, @unchecked Sendable { + let result: Bool +} diff --git a/DynamoxQuiz/Navigations/MainTabBarController.swift b/DynamoxQuiz/Navigations/MainTabBarController.swift new file mode 100644 index 0000000000..42ee3136cb --- /dev/null +++ b/DynamoxQuiz/Navigations/MainTabBarController.swift @@ -0,0 +1,74 @@ +// +// MainTabBarController.swift +// DynamoxQuiz +// +// Created by Mateus on 03/03/26. +// + +import UIKit + +class MainTabBarController: UITabBarController { + let flowController: AppCoordinator + let profileViewModel: ProfileViewModel + + init(profileViewModel: ProfileViewModel) { + self.profileViewModel = profileViewModel + self.flowController = AppCoordinator( + profileViewModel: profileViewModel + ) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func viewDidLoad() { + + super.viewDidLoad() + setupTabs() + setupAppearence() + } + + private func setupTabs() { + + let quizVC = QuizViewController(profileViewModel: profileViewModel) + let profileVC = ProfileViewController(profileViewModel: profileViewModel) + + quizVC.tabBarItem = UITabBarItem( + title: "Home", + image: UIImage(systemName: "house"), + tag: 0 + ) + + + profileVC.tabBarItem = UITabBarItem( + title: "Perfil", + image: UIImage(systemName: "person"), + tag: 1 + ) + viewControllers = [ + UINavigationController(rootViewController: quizVC), + UINavigationController(rootViewController: profileVC) + ] + } + + private func setupAppearence(){ + let apperance = UITabBarAppearance() + apperance.configureWithOpaqueBackground() + apperance.backgroundColor = .white + + apperance.stackedLayoutAppearance.selected.iconColor = Colors.primaryGreenBase + apperance.stackedLayoutAppearance.selected.titleTextAttributes = [ + .foregroundColor: Colors.primaryGreenBase + ] + + apperance.stackedLayoutAppearance.normal.iconColor = Colors.primaryGreenBase + apperance.stackedLayoutAppearance.normal.titleTextAttributes = [ + .foregroundColor: Colors.primaryGreenBase + ] + tabBar.standardAppearance = apperance + tabBar.scrollEdgeAppearance = apperance + } +} diff --git a/DynamoxQuiz/README.md b/DynamoxQuiz/README.md new file mode 100644 index 0000000000..05e9c08397 --- /dev/null +++ b/DynamoxQuiz/README.md @@ -0,0 +1,136 @@ +DynamoxQuiz — iOS Developer Challenge + +Sobre o Projeto +Aplicativo de Quiz iOS desenvolvido como solução ao Dynamox iOS Developer Challenge. +O app permite que o usuário informe seu nome, responda 10 perguntas de múltipla escolha obtidas via API, visualize o resultado ao final e consulte o histórico de partidas. + +## Screenshots + +

+ + + + + +

+ +Tecnologias Utilizadas + +Swift 6 +UIKit — telas de Splash e Quiz +SwiftUI — tela de Perfil +CocoaPods — gerenciamento de dependências +Alamofire — requisições HTTP +Core Data — persistência local de partidas e usuário +XCTest — testes unitários + + +## Arquitetura + +O projeto utiliza o padrão **MVVMC (Model-View-ViewModel-Coordinator)**: +``` +DynamoxQuiz/ +├── App/ # AppDelegate, SceneDelegate +├── Components/ # Componentes reutilizáveis de UI +├── Coordinators/ # Gerenciamento de navegação +├── Core/ +│ ├── Constants/ # Cores e constantes +│ ├── Network/ # APIService e protocolo APIServiceDelegate +│ └── Persistence/ # CoreDataStack e Repositories +├── Models/ # Question, AnswerResponse, QuizState +├── Views/ +│ ├── Splash/ # Tela inicial (UIKit) +│ ├── Quiz/ # Tela do quiz (UIKit) +│ └── Home/ # Tela de perfil (SwiftUI) +└── ViewsModels/ # ViewModels por tela +``` + +Decisões Técnicas + +Tive o MVVM-C como escolha para separar claramente a lógica de negócio da UI e da navegação, facilitando testes e manutenção. +Dependency Injection via init em todos os ViewModels e Repositories, permitindo o uso de Mocks nos testes. +Async/Await para todas as operações assíncronas de rede. +Protocolo APIServiceDelegate criado para desacoplar o ViewModel da implementação concreta do serviço, viabilizando testes sem chamadas reais de rede. +FileManager para persistência da foto de perfil — preferido ao UserDefaults por ser mais adequado para dados binários. + + +Funcionalidades + + Registro de nome/apelido do usuário + Carregamento de perguntas via API (GET /question) + Submissão de resposta e feedback imediato (POST /answer) + Navegação entre as 10 perguntas + Exibição do score final com opção de reiniciar + Persistência de partidas com Core Data + Histórico de partidas por usuário + Foto de perfil com PhotosPicker + FileManager + Testes unitários do QuizViewModel + + +Os testes cobrem a camada de maior risco e complexidade do negócio — o `QuizViewModel`. Utilizando **XCTest** com o padrão **AAA (Arrange, Act, Assert)** e Mocks para isolar dependências externas. + +``` +DynamoxQuizTests/ +├── Mocks/ +│ └── MockAPIService.swift # Mock do APIService para isolar rede +└── ViewModels/ + ├── NextQuestionTest.swift # Testa incremento do índice + ├── ResetQuizTest.swift # Testa reset de estado + └── LoadQuestionQuizTest.swift # Testa carregamento de pergunta com Mock +``` + +Casos testados + +test_quizViewModel_nextQuestion_sumNumberQuestion | Ao chamar nextQuestion(), o currentIndex deve incrementar em 1 | +test_quizViewModel_resetQuiz_deveZerarCurrentIndex | Após resetQuiz(), o currentIndex deve ser 0 | +test_quizViewModel_resetQuiz_deveZerarCorrectAnswerCount | Após resetQuiz(), o correctAnswerCount deve ser 0 | +test_quizViewModel_resetQuiz_deveZerarCurrentQuestion | Após resetQuiz(), o currentQuestion deve ser nil | +test_quizViewModel_resetQuiz_resetStateEqualQuiz | Após resetQuiz(), o state deve ser .quiz | +test_quizViewmodel_loadQuestionQuiz_shouldLoadQuestion | Ao carregar uma pergunta via Mock, currentQuestion não deve ser nil | +test_quizViewModel_loadQuestionQuiz_MustMaintainStateQuiz | Ao simular erro na API, o state deve permanecer .quiz e currentQuestion deve ser nil | + +Para rodar os testes: ⌘ + U no Xcode. + +Como Rodar o Projeto + +Requisitos: + +Xcode 15+ +iOS 16+ +CocoaPods instalado + +Instalação +``` +Clone o repositório +git clone https://github.com/TecoAdamo/developer-challenges.git + +# Entre na branch +git checkout mateus-adamo + +# Entre na pasta do projeto +cd DynamoxQuiz + +# Instale as dependências +pod install + +# Abra o workspace (não o .xcodeproj) +open DynamoxQuiz.xcworkspace +``` + +Rodando + +Selecione um simulador (iPhone 16 recomendado) +Pressione ⌘ + R para rodar +Para rodar os testes: ⌘ + U + + +Dependências (Podfile) +pod 'Alamofire' + +API +Base URL: https://quiz-api-bwi5hjqyaq-uc.a.run.app +MétodoEndpointDescriçãoGET/questionRetorna uma pergunta aleatóriaPOST/answer?questionId={id}Valida a resposta do usuário + +Autor +Mateus Adamo +Desenvolvido como solução ao Dynamox iOS Developer Challenge — Março 2026 diff --git a/DynamoxQuiz/Views/Home/HomeView.swift b/DynamoxQuiz/Views/Home/HomeView.swift new file mode 100644 index 0000000000..68a1d842d2 --- /dev/null +++ b/DynamoxQuiz/Views/Home/HomeView.swift @@ -0,0 +1,96 @@ +// +// HomeView.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class HomeView: UIView { + + public weak var delegate: HomeViewDelegate? + + let myButtonResponse = ButtonViewComponent(title: "Iniciar Quiz") + + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "Quiz" + label.font = .systemFont(ofSize: 22, weight: .bold) + label.textColor = .black + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + private let subLabel: UILabel = { + let label = UILabel() + label.text = "Teste seus conhecimentos com 10 perguntas" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = .gray + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let inputName: UITextField = { + let name = UITextField() + name.placeholder = "Informe seu nome: " + name.layer.cornerRadius = 6 + name.layer.borderWidth = 0.5 + name.translatesAutoresizingMaskIntoConstraints = false + name.returnKeyType = .done + + name.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 0)) + name.leftViewMode = .always + + return name + }() + + var userNickName: String { + inputName.text ?? "" + } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + + setupActions() + + setupUI() + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + private func setupActions(){ + myButtonResponse.setAction{ [weak self] in + self?.delegate?.didTapStart() + } + } + private func setupUI(){ + addSubview(titleLabel) + addSubview(subLabel) + addSubview(inputName) + addSubview(myButtonResponse) + + } + private func setupConstraints(){ + NSLayoutConstraint.activate([ + titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20), + + subLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), + subLabel.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor), + + inputName.topAnchor.constraint(equalTo: subLabel.bottomAnchor, constant: 12), + inputName.centerXAnchor.constraint(equalTo: subLabel.centerXAnchor), + inputName.widthAnchor.constraint(equalToConstant: 300), + inputName.heightAnchor.constraint(equalToConstant: 40), + + myButtonResponse.topAnchor.constraint(equalTo: inputName.bottomAnchor, constant: 12), + myButtonResponse.centerXAnchor.constraint(equalTo: inputName.centerXAnchor), + myButtonResponse.widthAnchor.constraint(equalToConstant: 300), + myButtonResponse.heightAnchor.constraint(equalToConstant: 40), + ]) + } +} diff --git a/DynamoxQuiz/Views/Home/HomeViewController.swift b/DynamoxQuiz/Views/Home/HomeViewController.swift new file mode 100644 index 0000000000..9021889e2b --- /dev/null +++ b/DynamoxQuiz/Views/Home/HomeViewController.swift @@ -0,0 +1,61 @@ +// +// HomeViewController.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class HomeViewController: UIViewController{ + + let homeView = HomeView() + let viewModel = HomeViewModel() + + let profileViewModel: ProfileViewModel + + public weak var flowDelegate: HomeFlowDelegate? + + override func loadView() { + view = homeView + } + + init(flowDelegate: HomeFlowDelegate, profileViewModel: ProfileViewModel){ + self.flowDelegate = flowDelegate + self.profileViewModel = profileViewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + homeView.delegate = self + bindViewModel() + } + + private func bindViewModel(){ + viewModel.succesResult = { [weak self] in + self?.flowDelegate?.navigateToQuiz() + } + + viewModel.showToast = { [weak self] message in + guard let self else { return } + ToastMessageComponent.show(message: message, in: self) + } + } +} +extension HomeViewController: HomeViewDelegate{ + + func didTapStart() { + let nickName = homeView.userNickName + + profileViewModel.user = nickName + UserDefaults.standard.set(nickName, forKey: "userName") + + viewModel.itsOkay(userNick: nickName) + } +} diff --git a/DynamoxQuiz/Views/Home/HomeViewDelegate.swift b/DynamoxQuiz/Views/Home/HomeViewDelegate.swift new file mode 100644 index 0000000000..431492db95 --- /dev/null +++ b/DynamoxQuiz/Views/Home/HomeViewDelegate.swift @@ -0,0 +1,16 @@ +// +// HomeViewDelegate.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import Foundation + +protocol HomeViewDelegate: AnyObject { + func didTapStart() +} + +public protocol HomeFlowDelegate: AnyObject { + func navigateToQuiz() +} diff --git a/DynamoxQuiz/Views/Profile/ProfileView.swift b/DynamoxQuiz/Views/Profile/ProfileView.swift new file mode 100644 index 0000000000..0d76243044 --- /dev/null +++ b/DynamoxQuiz/Views/Profile/ProfileView.swift @@ -0,0 +1,88 @@ +// +// ProfileView.swift +// DynamoxQuiz +// +// Created by Mateus on 03/03/26. +// + +import SwiftUI +import CoreData + +import PhotosUI + +struct ProfileView: View { + @ObservedObject var viewModel: ProfileViewModel + @State private var selectedPhoto: PhotosPickerItem? + @State private var profileImage: UIImage? + + + + var body: some View { + ZStack { + Color(Colors.lightGrayBase) + .ignoresSafeArea() + + ScrollView { + VStack { + + PhotosPicker(selection: $selectedPhoto, matching: .images) { [profileImage] in + ProfileImageView(image: profileImage) + } + + Text(viewModel.user) + .font(.title) + .bold() + .foregroundStyle(.black) + .padding() + + HStack(spacing: 16) { + CardViewComponent(title: "Jogados", value: "\(viewModel.totalPlay)") + } + + VStack { + Text("Histórico de Quizzes") + .font(.title3) + .bold() + .padding() + + ForEach(viewModel.match, id: \.objectID) { match in + matchCard(match: match) + } + + } + } + .frame(maxWidth: .infinity, alignment: .top) + .padding(.top, 20) + } + } + .onAppear { + viewModel.loadMatch() + profileImage = viewModel.loadProfileImage() + } + .onChange(of: selectedPhoto) { _ in + Task { + guard let item = selectedPhoto else { return } + + if let data = try? await item.loadTransferable(type: Data.self), + let image = UIImage(data: data) { + profileImage = image + viewModel.saveProfileImage(image) + } + } + } + } + @ViewBuilder + private func matchCard(match: NSManagedObject) -> some View { + let correct = match.value(forKey: "correctAnswers") as? Int16 ?? 0 + let total = match.value(forKey: "totalQuestions") as? Int16 ?? 0 + let date = match.value(forKey: "date") as? Date ?? Date() + let percent = total > 0 ? Int((Double(correct) / Double(total)) * 100) : 0 + + CardViewHistoryComponent( + icon: "trophy", + title: "Acertos: \(correct) de \(total) perguntas", + date: date.formatted(.dateTime.day().month()), + store: "\(percent)%" + ) + } +} diff --git a/DynamoxQuiz/Views/Profile/ProfileViewController.swift b/DynamoxQuiz/Views/Profile/ProfileViewController.swift new file mode 100644 index 0000000000..653f1fa975 --- /dev/null +++ b/DynamoxQuiz/Views/Profile/ProfileViewController.swift @@ -0,0 +1,48 @@ +// +// ProfileViewController.swift +// DynamoxQuiz +// +// Created by Mateus on 03/03/26. +// + +import UIKit +import SwiftUI + +class ProfileViewController: UIViewController { + + let profileView: ProfileViewModel + + init(profileViewModel: ProfileViewModel) { + self.profileView = profileViewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let profileView = ProfileView(viewModel: profileView) + let hostingController = UIHostingController(rootView: profileView) + + addChild(hostingController) + view.addSubview(hostingController.view) + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + hostingController.didMove(toParent: self) + } + private func goToQuiz() { + let quizVC = QuizViewController(profileViewModel: profileView) + navigationController?.pushViewController(quizVC, animated: false) + } +} diff --git a/DynamoxQuiz/Views/Quiz/QuizState.swift b/DynamoxQuiz/Views/Quiz/QuizState.swift new file mode 100644 index 0000000000..3c7e81fdd8 --- /dev/null +++ b/DynamoxQuiz/Views/Quiz/QuizState.swift @@ -0,0 +1,13 @@ +// +// QuizState.swift +// DynamoxQuiz +// +// Created by Mateus on 04/03/26. +// + +enum QuizState: Equatable { + case loading + case quiz + case result + case finished +} diff --git a/DynamoxQuiz/Views/Quiz/QuizView.swift b/DynamoxQuiz/Views/Quiz/QuizView.swift new file mode 100644 index 0000000000..da6c5f248f --- /dev/null +++ b/DynamoxQuiz/Views/Quiz/QuizView.swift @@ -0,0 +1,316 @@ +// +// QuizView.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class QuizView: UIView { + + weak var delegate: QuizViewDelegate? + + private let scrollView: UIScrollView = { + let scroll = UIScrollView() + scroll.translatesAutoresizingMaskIntoConstraints = false + scroll.showsVerticalScrollIndicator = true + return scroll + }() + + private let contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + let myButtonResponse = ButtonViewComponent(title: "Responder") + + let buttonRestartQuiz: ButtonViewComponent = { + let btn = ButtonViewComponent(title: "Refazer Quiz") + btn.isHidden = true + return btn + }() + + private let cardsStackView: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 20 + stack.distribution = .fill + stack.translatesAutoresizingMaskIntoConstraints = false + return stack + }() + + private var cards: [QuestionCardComponent] { + cardsStackView.arrangedSubviews.compactMap { $0 as? QuestionCardComponent } + } + + private let titleQuestionsLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 18, weight: .semibold) + label.textColor = .gray + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let questionsLabel: UILabel = { + + let label = UILabel() + label.text = "Carregando Quiz..." + label.font = .systemFont(ofSize: 22, weight: .bold) + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.textAlignment = .center + label.textColor = .black + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + let myPrimaryCardForQuestion = QuestionCardComponent(title: "...") + let mySecondaryCardForQuestion = QuestionCardComponent(title: "...") + let MyThirdCardForQuestion = QuestionCardComponent(title: "...") + let MyFourthCardForQuestion = QuestionCardComponent(title: "...") + let MyFiveCardForQuestion = QuestionCardComponent(title: "...") + + private let loadingView: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(style: .large) + indicator.color = Colors.primaryGreenBase + indicator.translatesAutoresizingMaskIntoConstraints = false + indicator.hidesWhenStopped = true + return indicator + }() + + private let logoWinnerView: UIImageView = { + let img = UIImageView() + img.image = UIImage(systemName: "trophy") + img.tintColor = Colors.primaryGreenBase + img.contentMode = .scaleAspectFit + img.translatesAutoresizingMaskIntoConstraints = false + img.isHidden = true + return img + }() + + let myCardResult: UIView = { + let card = UIView() + card.backgroundColor = .white + card.layer.cornerRadius = 12 + + card.layer.shadowColor = UIColor.black.cgColor + card.layer.shadowOpacity = 0.15 + card.layer.shadowOffset = CGSize(width: 0, height: 4) + card.layer.shadowRadius = 6 + + card.translatesAutoresizingMaskIntoConstraints = false + card.isHidden = true + return card + }() + + let textLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 22, weight: .bold) + label.textColor = .black + + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.textAlignment = .left + + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + return label + }() + + let subTextLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 20, weight: .medium) + label.textColor = .gray + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + return label + }() + + let percentTextLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 32, weight: .semibold) + label.textColor = Colors.primaryGreenBase + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + return label + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + setupUI() + setupActions() + + } + + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI(){ + backgroundColor = .white + + addSubview(scrollView) + addSubview(myButtonResponse) + addSubview(buttonRestartQuiz) + + scrollView.addSubview(contentView) + + contentView.addSubview(titleQuestionsLabel) + contentView.addSubview(questionsLabel) + contentView.addSubview(cardsStackView) + contentView.addSubview(loadingView) + + addSubview(logoWinnerView) + addSubview(myCardResult) + myCardResult.addSubview(textLabel) + myCardResult.addSubview(subTextLabel) + myCardResult.addSubview(percentTextLabel) + + cardsStackView.addArrangedSubview(myPrimaryCardForQuestion) + cardsStackView.addArrangedSubview(mySecondaryCardForQuestion) + cardsStackView.addArrangedSubview(MyThirdCardForQuestion) + cardsStackView.addArrangedSubview(MyFourthCardForQuestion) + cardsStackView.addArrangedSubview(MyFiveCardForQuestion) + + setupConstraints() + + } + + private func setupActions(){ + myButtonResponse.setAction { [ weak self ] in + self?.delegate?.didTapAnswerButton() + } + buttonRestartQuiz.setAction { [ weak self ] in + self?.delegate?.didTapRestartButton() + } + cards.enumerated().forEach { index, card in + card.onSelect = { [weak self ] in + self?.delegate?.didSelectOption(index: index) + } + } + } + + private func setupConstraints(){ + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: myButtonResponse.topAnchor, constant: -10), + + + contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + + loadingView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + loadingView.topAnchor.constraint(equalTo: questionsLabel.bottomAnchor, constant: 60), + + contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor), + + titleQuestionsLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + titleQuestionsLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + + questionsLabel.topAnchor.constraint(equalTo: titleQuestionsLabel.bottomAnchor, constant: 20), + questionsLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + questionsLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + + cardsStackView.topAnchor.constraint(equalTo: questionsLabel.bottomAnchor, constant: 30), + cardsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + cardsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + cardsStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20), + + myButtonResponse.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + myButtonResponse.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + myButtonResponse.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20), + myButtonResponse.heightAnchor.constraint(equalToConstant: 48), + + buttonRestartQuiz.centerXAnchor.constraint(equalTo: centerXAnchor), + buttonRestartQuiz.bottomAnchor.constraint(equalTo: myButtonResponse.bottomAnchor), + buttonRestartQuiz.widthAnchor.constraint(equalToConstant: 300), + buttonRestartQuiz.heightAnchor.constraint(equalToConstant: 48), + + logoWinnerView.bottomAnchor.constraint(equalTo: myCardResult.topAnchor, constant: -20), + logoWinnerView.centerXAnchor.constraint(equalTo: centerXAnchor), + logoWinnerView.widthAnchor.constraint(equalToConstant: 100), + logoWinnerView.heightAnchor.constraint(equalToConstant: 100), + + myCardResult.centerXAnchor.constraint(equalTo: centerXAnchor), + myCardResult.centerYAnchor.constraint(equalTo: centerYAnchor), + myCardResult.widthAnchor.constraint(equalToConstant: 300), + myCardResult.heightAnchor.constraint(equalToConstant: 250), + + + textLabel.topAnchor.constraint(equalTo: myCardResult.topAnchor, constant: 20), + textLabel.leadingAnchor.constraint(equalTo: myCardResult.leadingAnchor, constant: 16), + textLabel.trailingAnchor.constraint(equalTo: myCardResult.trailingAnchor, constant: -16), + + subTextLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 35), + subTextLabel.centerXAnchor.constraint(equalTo: myCardResult.centerXAnchor), + + percentTextLabel.topAnchor.constraint(equalTo: subTextLabel.bottomAnchor, constant: 10), + percentTextLabel.centerXAnchor.constraint(equalTo: myCardResult.centerXAnchor), + + percentTextLabel.bottomAnchor.constraint(equalTo: myCardResult.bottomAnchor) + ]) + } + + func updateQuestionTitle(number: String){ + titleQuestionsLabel.text = "Pergunta \(number) de 10" + } + func resetCards() { + cards.forEach{ $0.setSelection(false)} + } + func showRestartButton(){ + myButtonResponse.isHidden = true + buttonRestartQuiz.isHidden = false + } + func resetToQuizMode(){ + myButtonResponse.isHidden = false + buttonRestartQuiz.isHidden = true + } + func showCardResultTotal(){ + scrollView.isHidden = true + cardsStackView.isHidden = true + + titleQuestionsLabel.isHidden = true + questionsLabel.isHidden = true + myButtonResponse.isHidden = true + + logoWinnerView.isHidden = false + myCardResult.isHidden = false + textLabel.isHidden = false + subTextLabel.isHidden = false + percentTextLabel.isHidden = false + } + func showQuizAgain(){ + scrollView.isHidden = false + cardsStackView.isHidden = false + + cards.forEach { $0.setSelection(false)} + + titleQuestionsLabel.isHidden = false + questionsLabel.isHidden = false + + logoWinnerView.isHidden = true + myCardResult.isHidden = true + textLabel.isHidden = true + subTextLabel.isHidden = true + percentTextLabel.isHidden = true + } + func showLoading() { + cardsStackView.isHidden = true + questionsLabel.text = "" + loadingView.startAnimating() + } + + func hideLoading() { + cardsStackView.isHidden = false + loadingView.stopAnimating() + } +} diff --git a/DynamoxQuiz/Views/Quiz/QuizViewController.swift b/DynamoxQuiz/Views/Quiz/QuizViewController.swift new file mode 100644 index 0000000000..2cce13a9e2 --- /dev/null +++ b/DynamoxQuiz/Views/Quiz/QuizViewController.swift @@ -0,0 +1,232 @@ +// +// QuizViewController.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +final class QuizViewController: UIViewController { + + let viewModel = QuizViewModel() + let profileViewModel: ProfileViewModel + + private var hasSelectorAnswer: Bool = false + + init(profileViewModel: ProfileViewModel) { + self.profileViewModel = profileViewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var quizView: QuizView { + view as! QuizView + } + + private lazy var allCards: [QuestionCardComponent] = { + [ + quizView.myPrimaryCardForQuestion, + quizView.mySecondaryCardForQuestion, + quizView.MyThirdCardForQuestion, + quizView.MyFourthCardForQuestion, + quizView.MyFiveCardForQuestion, + ] + }() + + override func loadView() { + view = QuizView() + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.hidesBackButton = true + + Task { + await viewModel.loadQuestion() + } + + setupBindings() + setupSelectionLogic() + setupActions() + quizView.updateQuestionTitle(number: "\(viewModel.currentIndex + 1)") + } + + private func setupBindings(){ + viewModel.showToast = { [weak self] message in + guard let self else { return } + ToastMessageComponent.show(message: message, in: self) + } + + viewModel.onQuestionReceived = { [weak self] question in + guard let self = self else { return } + + self.quizView.updateQuestionTitle(number: "\(self.viewModel.currentIndex + 1)") + self.quizView.questionsLabel.text = question.statement + + for(index, card) in self.allCards.enumerated() { + if index < question.options.count { + card.configure(with: question.options[index]) + card.isHidden = false + + card.layer.borderWidth = 0.0 + card.layer.borderColor = nil + card.backgroundColor = .white + } else { + card.isHidden = true + } + } + } + viewModel.onAnswerResult = { [weak self] isCorrect in + guard let self = self else { return } + + let selectedCard = self.allCards.first(where: {$0.isSelected}) + + self.setCards(false) + + if isCorrect { + selectedCard?.backgroundColor = .systemGreen.withAlphaComponent(0.2) + selectedCard?.layer.borderColor = UIColor.systemGreen.cgColor + selectedCard?.layer.cornerRadius = 8 + selectedCard?.layer.borderWidth = 2 + } else { + selectedCard?.backgroundColor = .systemRed.withAlphaComponent(0.2) + selectedCard?.layer.borderColor = UIColor.systemRed.cgColor + selectedCard?.layer.cornerRadius = 8 + selectedCard?.layer.borderWidth = 2 + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + Task { [weak self] in + await self?.proceedToNextStep() + self?.setCards(true) + } + } + } + viewModel.onError = { message in + print("Erro ao carregar \(message)") + } + viewModel.onStateChange = { [weak self] state in + DispatchQueue.main.async { + switch state { + case .loading: + self?.quizView.showLoading() + case .quiz: + self?.quizView.hideLoading() + default: + break + } + } + } + } + + private func proceedToNextStep() async { + viewModel.nextQuestion() + updateQuestion() + + if viewModel.isQuizFinished { + finishQuiz() + } else { + await viewModel.loadQuestion() + } + + } + + private func setupActions() { + quizView.myButtonResponse.setAction { [weak self] in + self?.handleAnswerButtonTap() + } + + quizView.buttonRestartQuiz.setAction { [weak self] in + self?.restartQuiz() + } + } + private func setupSelectionLogic() { + allCards.forEach{ card in + card.onSelect = { [weak self] in + self?.selectCard(card) + } + } + } + private func handleAnswerButtonTap() { + let selectedComponent = allCards.first(where: {$0.isSelected}) + + guard let selectedCard = selectedComponent, let answer = selectedCard.getAnswerText() else { + ToastMessageComponent.show(message: "Selecione uma resposta antes de continuar!", in: self) + return + } + + guard let questionId = viewModel.currentQuestion?.id else { return } + + viewModel.answerQuestion(option: answer, questionId: questionId) + } + private func restartQuiz(){ + viewModel.resetQuiz() + viewModel.correctAnswerCount = 0 + hasSelectorAnswer = false + quizView.updateQuestionTitle(number: "1") + quizView.resetCards() + quizView.resetToQuizMode() + + setCards(true) + quizView.showQuizAgain() + Task { + await viewModel.loadQuestion() + } + } + private func updateQuestion(){ + quizView.updateQuestionTitle(number: "\(viewModel.currentIndex + 1)") + quizView.resetCards() + hasSelectorAnswer = false + } + + private func selectCard(_ selectedCard: QuestionCardComponent){ + allCards.forEach{$0.setSelection(false) } + selectedCard.setSelection(true) + hasSelectorAnswer = true + } + + + private func setCards(_ enable: Bool){ + allCards.forEach{ + $0.isUserInteractionEnabled = enable + $0.alpha = enable ? 1.0 : 0.5 + } + + quizView.myButtonResponse.isUserInteractionEnabled = enable + quizView.myButtonResponse.alpha = enable ? 1.0 : 0.5 + } + private func setupShowTotal(_ hidden: Bool){ + allCards.forEach{ + $0.isUserInteractionEnabled = hidden + $0.alpha = hidden ? 1.0 : 0.5 + } + } + private func finishQuiz() { + + let nickUserName = profileViewModel.user + let correctAnswers = viewModel.correctAnswerCount + let total = 10 + let porcentam = (Double(correctAnswers) / Double(total)) * 100.0 + + viewModel.saveResult(name: nickUserName, correct: Int16(correctAnswers), total: Int16(total), rounds: Int32(viewModel.currentIndex)) + + if correctAnswers < 6 { + quizView.textLabel.text = "Não foi dessa vez \(nickUserName), mas você está no caminho certo 💪" + } else if correctAnswers < 8 { + quizView.textLabel.text = "Mandou bem \(nickUserName)! Dá pra melhorar ainda mais 🚀" + } else { + quizView.textLabel.text = "Arrasou \(nickUserName)! Desempenho incrível 🎉" + } + quizView.subTextLabel.text = "Vc acertou \(correctAnswers) de \(total)" + quizView.percentTextLabel.text = "\(Int(porcentam))% de acerto" + + + quizView.showCardResultTotal() + quizView.showRestartButton() + } +} + diff --git a/DynamoxQuiz/Views/Quiz/QuizViewDelegate.swift b/DynamoxQuiz/Views/Quiz/QuizViewDelegate.swift new file mode 100644 index 0000000000..de16aaf22e --- /dev/null +++ b/DynamoxQuiz/Views/Quiz/QuizViewDelegate.swift @@ -0,0 +1,12 @@ +// +// QuizFlowDelegate.swift +// DynamoxQuiz +// +// Created by Mateus on 04/03/26. +// + +protocol QuizViewDelegate: AnyObject { + func didTapAnswerButton() + func didTapRestartButton() + func didSelectOption(index: Int) +} diff --git a/DynamoxQuiz/Views/Splash/SplashFlowDelegate.swift b/DynamoxQuiz/Views/Splash/SplashFlowDelegate.swift new file mode 100644 index 0000000000..81932ff00d --- /dev/null +++ b/DynamoxQuiz/Views/Splash/SplashFlowDelegate.swift @@ -0,0 +1,12 @@ +// +// SplashFlowDelegate.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import Foundation + +public protocol SplashFlowDelegate: AnyObject { + func navigateToHome() +} diff --git a/DynamoxQuiz/Views/Splash/SplashView.swift b/DynamoxQuiz/Views/Splash/SplashView.swift new file mode 100644 index 0000000000..55a68905bc --- /dev/null +++ b/DynamoxQuiz/Views/Splash/SplashView.swift @@ -0,0 +1,50 @@ +// +// SplashView.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class SplashView: UIView{ + private let logoImageView: UIImageView = { + let img = UIImageView() + img.image = UIImage(named: "LogoSplash") + img.contentMode = .scaleAspectFit + img.layer.cornerRadius = 12 + img.clipsToBounds = true + img.translatesAutoresizingMaskIntoConstraints = false + return img + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI(){ + backgroundColor = .systemBackground + + addSubview(logoImageView) + + NSLayoutConstraint.activate([ + logoImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + logoImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + logoImageView.widthAnchor.constraint(equalToConstant: 120), + logoImageView.heightAnchor.constraint(equalToConstant: 120) + ]) + } + func animateLogo() { + logoImageView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7) + + UIView.animate(withDuration: 1.0, delay: 0.2, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: { + self.logoImageView.transform = .identity + }) + } +} + diff --git a/DynamoxQuiz/Views/Splash/SplashViewController.swift b/DynamoxQuiz/Views/Splash/SplashViewController.swift new file mode 100644 index 0000000000..3a6fe5ca90 --- /dev/null +++ b/DynamoxQuiz/Views/Splash/SplashViewController.swift @@ -0,0 +1,35 @@ +// +// SplashViewController.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import UIKit + +class SplashViewController: UIViewController { + + let contentView = SplashView() + let viewModel = HomeViewModel() + + public weak var flowDelegate: SplashFlowDelegate? + + init(flowDelegate: SplashFlowDelegate? = nil) { + self.flowDelegate = flowDelegate + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = contentView + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + contentView.animateLogo() + } +} diff --git a/DynamoxQuiz/Views/ViewsModels/HomeModel/HomeViewModel.swift b/DynamoxQuiz/Views/ViewsModels/HomeModel/HomeViewModel.swift new file mode 100644 index 0000000000..dca595f88a --- /dev/null +++ b/DynamoxQuiz/Views/ViewsModels/HomeModel/HomeViewModel.swift @@ -0,0 +1,22 @@ +// +// HomeViewModel.swift +// DynamoxQuiz +// +// Created by Mateus on 02/03/26. +// + +import Foundation + +class HomeViewModel { + var succesResult: (() -> Void)? + var showToast: ((String) -> Void)? + + func itsOkay(userNick: String){ + if userNick.isEmpty { + showToast?("Informe seu nome para prosseguir.") + } else { + succesResult?() + } + } +} + diff --git a/DynamoxQuiz/Views/ViewsModels/Profile/ProfileViewModel.swift b/DynamoxQuiz/Views/ViewsModels/Profile/ProfileViewModel.swift new file mode 100644 index 0000000000..0260df95a1 --- /dev/null +++ b/DynamoxQuiz/Views/ViewsModels/Profile/ProfileViewModel.swift @@ -0,0 +1,75 @@ +// +// ProfileViewModel.swift +// DynamoxQuiz +// +// Created by Mateus on 05/03/26. +// + +import SwiftUI +import Combine +import CoreData + +final class ProfileViewModel: ObservableObject { + private let userDefaultsKey = "savedUserName" + + @Published var user: String { + didSet { + UserDefaults.standard.set(user, forKey: userDefaultsKey) + } + } + + @Published var match: [NSManagedObject] = [] + + private let repository: QuizRepository + + init(repository: QuizRepository = QuizRepository()) { + self.user = UserDefaults.standard.string(forKey: userDefaultsKey) ?? "" + self.repository = repository + } + + var hasUser: Bool { + !user.trimmingCharacters(in: .whitespaces).isEmpty + } + + var totalPlay: Int { + match.count + } + + var correctPlay: String { + guard !match.isEmpty else { return "0%" } + + let totalCorrect = match.compactMap{ $0.value(forKey: "correctAnswer") as? Int16 } + let totalQuestions = match.compactMap{ $0.value(forKey: "totalQuestions") as? Int16 } + + let sumCorrect = totalCorrect.reduce(0, +) + let sumTotal = totalQuestions.reduce(0, +) + + guard sumTotal > 0 else { return "0%" } + + let media = (Double(sumCorrect) / Double(sumTotal)) * 100.0 + return "\(Int(media))%" + } + func loadMatch() { + match = repository.search() + } + + func saveProfileImage(_ image: UIImage){ + guard let data = image.jpegData(compressionQuality: 0.8) else { return } + + let url = getImageUrl() + try? data.write(to: url) + } + + func loadProfileImage() -> UIImage? { + let url = getImageUrl() + + guard let data = try? Data(contentsOf: url) else { return nil } + + return UIImage(data: data) + } + + private func getImageUrl() -> URL { + FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("profileImage.jpg") + } +} diff --git a/DynamoxQuiz/Views/ViewsModels/QuizModel/QuizViewModel.swift b/DynamoxQuiz/Views/ViewsModels/QuizModel/QuizViewModel.swift new file mode 100644 index 0000000000..6a67dc08e5 --- /dev/null +++ b/DynamoxQuiz/Views/ViewsModels/QuizModel/QuizViewModel.swift @@ -0,0 +1,104 @@ +// +// QuizViewModel.swift +// DynamoxQuiz +// +// Created by Mateus on 03/03/26. +// + +import Foundation + +class QuizViewModel { + + private let service: APIServiceDelegate + private let repository: QuizRepository + + private let totalQuestions: Int = 10 + private(set) var currentIndex = 0 + + var currentQuestion: Question? + var correctAnswerCount: Int = 0 + var onStateChange: ((QuizState) -> Void)? + + var onQuestionReceived: ((Question) -> Void)? + var onError: ((String) -> Void)? + var onLoadingChange: ((Bool) -> Void)? + var onAnswerResult: ((Bool) -> Void)? + + var showToast: ((String) -> Void)? + + init( + service: APIServiceDelegate = APIService(), + repository: QuizRepository = QuizRepository() + ){ + self.service = service + self.repository = repository + } + + private(set) var state: QuizState = .quiz { + didSet { + onStateChange?(state) + } + } + + var isQuizFinished: Bool { + currentIndex >= totalQuestions + } + + func nextQuestion() { + currentIndex += 1 + + } + + func resetQuiz(){ + currentIndex = 0 + correctAnswerCount = 0 + currentQuestion = nil + state = .quiz + } + + func saveResult(name: String, correct: Int16, total: Int16, rounds: Int32){ + repository.create( + name: name, + correct: correct, + total: total, + rounds: rounds + ) + } + + func loadQuestion() async { + state = .loading + do { + let question = try await service.fetchRandomQuestion() + currentQuestion = question + await MainActor.run{ + onQuestionReceived?(question) + state = .quiz + } + } catch { + await MainActor.run{ + onError?(error.localizedDescription) + state = .quiz + } + } + } + func answerQuestion(option: String, questionId: String){ + Task { + do { + let isCorrect = try await service.validateAnswer(questionId: questionId, answer: option) + + if isCorrect { + self.correctAnswerCount += 1 + self.showToast?("Resposta correta! ✅") + } else { + self.showToast?("Resposta errada! ❌") + } + + await MainActor.run{ + self.onAnswerResult?(isCorrect) + } + } catch { + self.onError?("Erro ao validar") + } + } + } +} diff --git a/DynamoxQuizTests/ViewModels/Mocks/MockApiService.swift b/DynamoxQuizTests/ViewModels/Mocks/MockApiService.swift new file mode 100644 index 0000000000..29e3faed62 --- /dev/null +++ b/DynamoxQuizTests/ViewModels/Mocks/MockApiService.swift @@ -0,0 +1,26 @@ +// +// ApiServiceTest.swift +// DynamoxQuizTests +// +// Created by Mateus on 07/03/26. +// + +import XCTest +@testable import DynamoxQuiz + +final class MockApiService: APIServiceDelegate { + + var questionMock: Question? + var shouldThrowError = false + + func fetchRandomQuestion() async throws -> Question { + if shouldThrowError { throw NSError(domain: "test", code: 0) } + return questionMock! + } + + + func validateAnswer(questionId: String, answer: String) async throws -> Bool { + return true + } + +} diff --git a/DynamoxQuizTests/ViewModels/QuizViewModelTests/LoadQuestionQuizTest.swift b/DynamoxQuizTests/ViewModels/QuizViewModelTests/LoadQuestionQuizTest.swift new file mode 100644 index 0000000000..c210fdc5b3 --- /dev/null +++ b/DynamoxQuizTests/ViewModels/QuizViewModelTests/LoadQuestionQuizTest.swift @@ -0,0 +1,53 @@ +// +// LoadQuestionQuizTest.swift +// DynamoxQuizTests +// +// Created by Mateus on 07/03/26. +// + +import XCTest +@testable import DynamoxQuiz + +@MainActor +final class LoadQuestionQuizTest: XCTestCase { + + + var mock: MockApiService! + var sut: QuizViewModel! + + override func setUp() { + super.setUp() + mock = MockApiService() + sut = QuizViewModel(service: mock) + } + + override func tearDown() { + sut = nil + mock = nil + super.tearDown() + } + + func test_quizViewmodel_loadQuestionQuiz_shouldLoadQuestion() async { + mock.questionMock = Question(id: "1", statement: "Pergunta teste", options: ["A", "B", "C"]) + + await sut.loadQuestion() + + + XCTAssertNotNil(sut.currentQuestion) + XCTAssertEqual(sut.state, .quiz) + } + + func test_quizViewModel_loadQuestionQuiz_MustMaintainStateQuiz() async { + + mock.shouldThrowError = true + + await sut.loadQuestion() + + XCTAssertEqual(sut.state, .quiz) + XCTAssertNil(sut.currentQuestion) + + + + } + +} diff --git a/DynamoxQuizTests/ViewModels/QuizViewModelTests/NextQuestionTest.swift b/DynamoxQuizTests/ViewModels/QuizViewModelTests/NextQuestionTest.swift new file mode 100644 index 0000000000..c6ee8c8e1b --- /dev/null +++ b/DynamoxQuizTests/ViewModels/QuizViewModelTests/NextQuestionTest.swift @@ -0,0 +1,37 @@ +// +// NextQuestionTest.swift +// DynamoxQuizTests +// +// Created by Mateus on 07/03/26. +// + +import XCTest +@testable import DynamoxQuiz + +final class NextQuestionTest: XCTestCase { + + var sut: QuizViewModel! + + override func setUp(){ + super.setUp() + sut = QuizViewModel() + } + + func test_quizViewModel_nextQuestion_sumNumberQuestion(){ + let initialIndex = sut.currentIndex + + sut.nextQuestion() + + XCTAssertEqual(sut.currentIndex, initialIndex + 1) } + + func test_quizViewModel_isQuizFinished_mustBeFalseAtTheBeginning(){ + XCTAssertFalse(sut.isQuizFinished) + } + + func test_quizViewModel_isQuizFinished_ItMustBeTrueWhenYouReachTen(){ + for _ in 0..<10 { + sut.nextQuestion() + } + XCTAssertTrue(sut.isQuizFinished) + } +} diff --git a/DynamoxQuizTests/ViewModels/QuizViewModelTests/ResetQuizTest.swift b/DynamoxQuizTests/ViewModels/QuizViewModelTests/ResetQuizTest.swift new file mode 100644 index 0000000000..bd8b18eed5 --- /dev/null +++ b/DynamoxQuizTests/ViewModels/QuizViewModelTests/ResetQuizTest.swift @@ -0,0 +1,48 @@ +// +// QuizViewModelTests.swift +// DynamoxQuiz +// +// Created by Mateus on 06/03/26. +// + +import XCTest +@testable import DynamoxQuiz + +final class ResetQuizTest: XCTestCase { + + var sut: QuizViewModel! + + override func setUp(){ + super.setUp() + sut = QuizViewModel() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + func test_quizViewModel_resetQuiz_resetCurrentIndex(){ + sut.resetQuiz() + + XCTAssertEqual(sut.currentIndex, 0) + } + + func test_quizViewModel_resetQuiz_resetCorrectAnswerCount(){ + sut.resetQuiz() + + XCTAssertEqual(sut.correctAnswerCount, 0) + } + + func test_quizViewModel_resetQuiz_resetCurrentQuestion(){ + sut.resetQuiz() + + XCTAssertNil(sut.currentQuestion) + } + @MainActor + func test_quizViewModel_resetQuiz_resetStateEqualQuiz(){ + sut.resetQuiz() + + XCTAssertEqual(sut.state, .quiz) + } +} diff --git a/Podfile b/Podfile new file mode 100644 index 0000000000..09187dd0ce --- /dev/null +++ b/Podfile @@ -0,0 +1,25 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '16.0' + +target 'DynamoxQuiz' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + pod 'Alamofire' + + # Pods for DynamoxQuiz +end + +target 'DynamoxQuizTests' do + inherit! :search_paths + pod 'Alamofire' +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + config.build_settings['SWIFT_SUPPRESS_WARNINGS'] = 'YES' + end + end +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000000..6f3b09f925 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - Alamofire (5.11.1) + +DEPENDENCIES: + - Alamofire + +SPEC REPOS: + trunk: + - Alamofire + +SPEC CHECKSUMS: + Alamofire: eec6cd8f73b242b59e34153a606a909eb9864b14 + +PODFILE CHECKSUM: 7ff698ea81bb4da02a1206b76fe9962f7eeb02d6 + +COCOAPODS: 1.16.2