From 891be2fc8cbf6fd602d48df2669333e77c0da5cd Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 14:11:01 -0300 Subject: [PATCH 01/13] Initial commit. - Blank project. - Added .gitignore --- .gitignore | 2 + .../project.pbxproj | 568 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 14125 bytes .../xcschemes/xcschememanagement.plist | 14 + busbud-coding-challenge/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + busbud-coding-challenge/Info.plist | 25 + busbud-coding-challenge/SceneDelegate.swift | 52 ++ busbud-coding-challenge/ViewController.swift | 19 + .../busbud_coding_challengeTests.swift | 17 + .../busbud_coding_challengeUITests.swift | 43 ++ ...d_coding_challengeUITestsLaunchTests.swift | 33 + 17 files changed, 917 insertions(+) create mode 100644 .gitignore create mode 100644 busbud-coding-challenge.xcodeproj/project.pbxproj create mode 100644 busbud-coding-challenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 busbud-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/spencerdiniz.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 busbud-coding-challenge/AppDelegate.swift create mode 100644 busbud-coding-challenge/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 busbud-coding-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 busbud-coding-challenge/Assets.xcassets/Contents.json create mode 100644 busbud-coding-challenge/Base.lproj/LaunchScreen.storyboard create mode 100644 busbud-coding-challenge/Base.lproj/Main.storyboard create mode 100644 busbud-coding-challenge/Info.plist create mode 100644 busbud-coding-challenge/SceneDelegate.swift create mode 100644 busbud-coding-challenge/ViewController.swift create mode 100644 busbud-coding-challengeTests/busbud_coding_challengeTests.swift create mode 100644 busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift create mode 100644 busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ca0973 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store + diff --git a/busbud-coding-challenge.xcodeproj/project.pbxproj b/busbud-coding-challenge.xcodeproj/project.pbxproj new file mode 100644 index 0000000..97af442 --- /dev/null +++ b/busbud-coding-challenge.xcodeproj/project.pbxproj @@ -0,0 +1,568 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + 14BBD4632D11E6250047FE7A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 14BBD4442D11E6240047FE7A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 14BBD44B2D11E6240047FE7A; + remoteInfo = "busbud-coding-challenge"; + }; + 14BBD46D2D11E6250047FE7A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 14BBD4442D11E6240047FE7A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 14BBD44B2D11E6240047FE7A; + remoteInfo = "busbud-coding-challenge"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 14BBD44C2D11E6240047FE7A /* busbud-coding-challenge.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "busbud-coding-challenge.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14BBD4622D11E6250047FE7A /* busbud-coding-challengeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "busbud-coding-challengeTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14BBD46C2D11E6250047FE7A /* busbud-coding-challengeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "busbud-coding-challengeUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 14BBD4742D11E6250047FE7A /* Exceptions for "busbud-coding-challenge" folder in "busbud-coding-challenge" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 14BBD44E2D11E6240047FE7A /* busbud-coding-challenge */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 14BBD4742D11E6250047FE7A /* Exceptions for "busbud-coding-challenge" folder in "busbud-coding-challenge" target */, + ); + path = "busbud-coding-challenge"; + sourceTree = ""; + }; + 14BBD4652D11E6250047FE7A /* busbud-coding-challengeTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "busbud-coding-challengeTests"; + sourceTree = ""; + }; + 14BBD46F2D11E6250047FE7A /* busbud-coding-challengeUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "busbud-coding-challengeUITests"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 14BBD4492D11E6240047FE7A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 14BBD45F2D11E6250047FE7A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 14BBD4692D11E6250047FE7A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 14BBD4432D11E6240047FE7A = { + isa = PBXGroup; + children = ( + 14BBD44E2D11E6240047FE7A /* busbud-coding-challenge */, + 14BBD4652D11E6250047FE7A /* busbud-coding-challengeTests */, + 14BBD46F2D11E6250047FE7A /* busbud-coding-challengeUITests */, + 14BBD44D2D11E6240047FE7A /* Products */, + ); + sourceTree = ""; + }; + 14BBD44D2D11E6240047FE7A /* Products */ = { + isa = PBXGroup; + children = ( + 14BBD44C2D11E6240047FE7A /* busbud-coding-challenge.app */, + 14BBD4622D11E6250047FE7A /* busbud-coding-challengeTests.xctest */, + 14BBD46C2D11E6250047FE7A /* busbud-coding-challengeUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */ = { + isa = PBXNativeTarget; + buildConfigurationList = 14BBD4752D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challenge" */; + buildPhases = ( + 14BBD4482D11E6240047FE7A /* Sources */, + 14BBD4492D11E6240047FE7A /* Frameworks */, + 14BBD44A2D11E6240047FE7A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 14BBD44E2D11E6240047FE7A /* busbud-coding-challenge */, + ); + name = "busbud-coding-challenge"; + packageProductDependencies = ( + ); + productName = "busbud-coding-challenge"; + productReference = 14BBD44C2D11E6240047FE7A /* busbud-coding-challenge.app */; + productType = "com.apple.product-type.application"; + }; + 14BBD4612D11E6250047FE7A /* busbud-coding-challengeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 14BBD47A2D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challengeTests" */; + buildPhases = ( + 14BBD45E2D11E6250047FE7A /* Sources */, + 14BBD45F2D11E6250047FE7A /* Frameworks */, + 14BBD4602D11E6250047FE7A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 14BBD4642D11E6250047FE7A /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 14BBD4652D11E6250047FE7A /* busbud-coding-challengeTests */, + ); + name = "busbud-coding-challengeTests"; + packageProductDependencies = ( + ); + productName = "busbud-coding-challengeTests"; + productReference = 14BBD4622D11E6250047FE7A /* busbud-coding-challengeTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 14BBD46B2D11E6250047FE7A /* busbud-coding-challengeUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 14BBD47D2D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challengeUITests" */; + buildPhases = ( + 14BBD4682D11E6250047FE7A /* Sources */, + 14BBD4692D11E6250047FE7A /* Frameworks */, + 14BBD46A2D11E6250047FE7A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 14BBD46E2D11E6250047FE7A /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 14BBD46F2D11E6250047FE7A /* busbud-coding-challengeUITests */, + ); + name = "busbud-coding-challengeUITests"; + packageProductDependencies = ( + ); + productName = "busbud-coding-challengeUITests"; + productReference = 14BBD46C2D11E6250047FE7A /* busbud-coding-challengeUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 14BBD4442D11E6240047FE7A /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 14BBD44B2D11E6240047FE7A = { + CreatedOnToolsVersion = 16.2; + }; + 14BBD4612D11E6250047FE7A = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 14BBD44B2D11E6240047FE7A; + }; + 14BBD46B2D11E6250047FE7A = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 14BBD44B2D11E6240047FE7A; + }; + }; + }; + buildConfigurationList = 14BBD4472D11E6240047FE7A /* Build configuration list for PBXProject "busbud-coding-challenge" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 14BBD4432D11E6240047FE7A; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 14BBD44D2D11E6240047FE7A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */, + 14BBD4612D11E6250047FE7A /* busbud-coding-challengeTests */, + 14BBD46B2D11E6250047FE7A /* busbud-coding-challengeUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 14BBD44A2D11E6240047FE7A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 14BBD4602D11E6250047FE7A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 14BBD46A2D11E6250047FE7A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 14BBD4482D11E6240047FE7A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 14BBD45E2D11E6250047FE7A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 14BBD4682D11E6250047FE7A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 14BBD4642D11E6250047FE7A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */; + targetProxy = 14BBD4632D11E6250047FE7A /* PBXContainerItemProxy */; + }; + 14BBD46E2D11E6250047FE7A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */; + targetProxy = 14BBD46D2D11E6250047FE7A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 14BBD4762D11E6250047FE7A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challenge"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 14BBD4772D11E6250047FE7A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challenge"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 14BBD4782D11E6250047FE7A /* 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; + 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 = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 14BBD4792D11E6250047FE7A /* 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"; + 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 = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 14BBD47B2D11E6250047FE7A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challengeTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/busbud-coding-challenge.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/busbud-coding-challenge"; + }; + name = Debug; + }; + 14BBD47C2D11E6250047FE7A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challengeTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/busbud-coding-challenge.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/busbud-coding-challenge"; + }; + name = Release; + }; + 14BBD47E2D11E6250047FE7A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challengeUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "busbud-coding-challenge"; + }; + name = Debug; + }; + 14BBD47F2D11E6250047FE7A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challengeUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "busbud-coding-challenge"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 14BBD4472D11E6240047FE7A /* Build configuration list for PBXProject "busbud-coding-challenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 14BBD4782D11E6250047FE7A /* Debug */, + 14BBD4792D11E6250047FE7A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 14BBD4752D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 14BBD4762D11E6250047FE7A /* Debug */, + 14BBD4772D11E6250047FE7A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 14BBD47A2D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challengeTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 14BBD47B2D11E6250047FE7A /* Debug */, + 14BBD47C2D11E6250047FE7A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 14BBD47D2D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challengeUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 14BBD47E2D11E6250047FE7A /* Debug */, + 14BBD47F2D11E6250047FE7A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 14BBD4442D11E6240047FE7A /* Project object */; +} diff --git a/busbud-coding-challenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/busbud-coding-challenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/busbud-coding-challenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/busbud-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/spencerdiniz.xcuserdatad/UserInterfaceState.xcuserstate b/busbud-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/spencerdiniz.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..ca76cb0da20dad9dcf82962b4a5ef6d7ab7a2025 GIT binary patch literal 14125 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 literal 0 HcmV?d00001 diff --git a/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist b/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..bbe2434 --- /dev/null +++ b/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + busbud-coding-challenge.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/busbud-coding-challenge/AppDelegate.swift b/busbud-coding-challenge/AppDelegate.swift new file mode 100644 index 0000000..5399603 --- /dev/null +++ b/busbud-coding-challenge/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +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/busbud-coding-challenge/Assets.xcassets/AccentColor.colorset/Contents.json b/busbud-coding-challenge/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/busbud-coding-challenge/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/busbud-coding-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json b/busbud-coding-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/busbud-coding-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/busbud-coding-challenge/Assets.xcassets/Contents.json b/busbud-coding-challenge/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/busbud-coding-challenge/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/busbud-coding-challenge/Base.lproj/LaunchScreen.storyboard b/busbud-coding-challenge/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/busbud-coding-challenge/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/busbud-coding-challenge/Base.lproj/Main.storyboard b/busbud-coding-challenge/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/busbud-coding-challenge/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/busbud-coding-challenge/Info.plist b/busbud-coding-challenge/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/busbud-coding-challenge/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/busbud-coding-challenge/SceneDelegate.swift b/busbud-coding-challenge/SceneDelegate.swift new file mode 100644 index 0000000..8eb4a02 --- /dev/null +++ b/busbud-coding-challenge/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + 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/busbud-coding-challenge/ViewController.swift b/busbud-coding-challenge/ViewController.swift new file mode 100644 index 0000000..49c45ff --- /dev/null +++ b/busbud-coding-challenge/ViewController.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/busbud-coding-challengeTests/busbud_coding_challengeTests.swift b/busbud-coding-challengeTests/busbud_coding_challengeTests.swift new file mode 100644 index 0000000..c38062f --- /dev/null +++ b/busbud-coding-challengeTests/busbud_coding_challengeTests.swift @@ -0,0 +1,17 @@ +// +// busbud_coding_challengeTests.swift +// busbud-coding-challengeTests +// +// Created by Spencer Diniz on 17/12/24. +// + +import Testing +@testable import busbud_coding_challenge + +struct busbud_coding_challengeTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift b/busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift new file mode 100644 index 0000000..89359e7 --- /dev/null +++ b/busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift @@ -0,0 +1,43 @@ +// +// busbud_coding_challengeUITests.swift +// busbud-coding-challengeUITests +// +// Created by Spencer Diniz on 17/12/24. +// + +import XCTest + +final class busbud_coding_challengeUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift b/busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift new file mode 100644 index 0000000..58d266a --- /dev/null +++ b/busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// busbud_coding_challengeUITestsLaunchTests.swift +// busbud-coding-challengeUITests +// +// Created by Spencer Diniz on 17/12/24. +// + +import XCTest + +final class busbud_coding_challengeUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} From 0d9186f7ae7388b1209e939b92f14a7b3c64373d Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 14:15:06 -0300 Subject: [PATCH 02/13] Ignore xcuserdata directory --- .gitignore | 1 + .../UserInterfaceState.xcuserstate | Bin 14125 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 busbud-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/spencerdiniz.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/.gitignore b/.gitignore index 5ca0973..97d23f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store +busbud-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/ diff --git a/busbud-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/spencerdiniz.xcuserdatad/UserInterfaceState.xcuserstate b/busbud-coding-challenge.xcodeproj/project.xcworkspace/xcuserdata/spencerdiniz.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index ca76cb0da20dad9dcf82962b4a5ef6d7ab7a2025..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14125 zcmd6N2UwF=+y5B?O2`5NNg$9#AQZw9aI{sXVihR?997dm9x)XNCPAgH6I-oqZMDN% zJ0)1PcJF0dYp31S)~>g2_g+@J=XamyNf=h!{{Qdwz1IiVmE<|+e$Kep`Q7)qTe~~F zfnZ_b%LpTaC>nucP%Mf=@iVQ91%JTn>zZYC``Z?H;i<_Q^mWg)`dZHr+Jb>Zgx79$ zsg(83Q-of@QxPG035`Ufnw>#+P+<5LH7-Ijlz>u^7U@tva-afKh>B1#DnX-BDJnzd zXbhTyPC--AG*pkKqXu*;It|T1bJ09>23m+ZP$%j_ezX+zqE(2Yv(XxK9=ZTsh%QB& z&}HZvbS>J7`q6fDJ-QLygq}oCp?&CS^bC3yJ%^r0FQ6CEOK3kjfL=zgpo8dN=q>af z`T~84zCvH4L+Bgy3;GrPhA~#-M4W_^u?DB$RIJ5jY{6EXh3$AOuEDi<3U0us;%3}} z=is^6jRia(FT_6FgBRmod?sFomt%&{#_RA!_;P#|-irHh7;ne7;@j};_zrvzeh@!| zAI4AOr|>@fG=2uZh+o35;MegR_+9)V{uFQ^_<^Po|Rwaw=&iv&dYsf~+K~2qPh~nw&+>CTqxA zat>KX&L!uO^T|eXDcMA>B>iLu*-36EcaS^DUF1IU5P6t9NuDD6$kXH*@+^6gyg}Y1 z{~~XZcgaWOWAX_(M7|-1$r17c`HlQe{-BtSpd+b*Cen0jpcyoiW>Fi>rTMgkj;3Si ziL{#5&{|qYr&13U=zQ8v7f>%fgD#{Uw3BvGAMK`#sGkOCkoM4}^lZ9@uBGSDb@W_% z9zCC4M6aY*(W~h-^jf-=?xHu)8|kg|PWl*qoIXLHq)*X(^x0->S5HUB0VGEXq(mx| zgi>bII~@y!-oR}5j_7NPwXMS)2!xRuB{ITj7$qYO8^P{nW|zu3y1cZeu(q@)zr3ux zDt~l&QF(r4;n?#0+S=mU^6HZE!m_ayE>&_}bwfgA1JAuJ#TAdR(eZAgpK$g3%A` z7TT+PT|vLEqeJk!R44PHqMR7sS2?z*vZ|_lY<@{uWoiEClESL|v1KK-`8Abgg(X#$ z6{Y3Xg)WtDa4kMxN2}Xk7hQ`>r5)@O*E!HN(xqZ!j75co#^S=!g~pC>f-^IZ8=GhD^7%X69o{nqj}t!3H!on3R%qv!mipqN|sz7-=&^R<6orq3C zC!+~yB2%+Omc){ohNZC79U$K-RE=s-E&Q4c($%tj=3u4ptBir1Rj^U#0^gE)_hN6m z8w4gb4Lnx(1rVDWsG9kBPjSqiB|SbgH^zR1G7za2Z94L@n)AQrMYYVF^RR(7=ov{ZumIS_4rUXO4+)V znfIdr3Zfpgc(&v(y1>$ANU z?CEO!uh!@(jtLzR6Kk}9f`GFNRuv^A~1$gF>gc`MH6Joa&$@FfY)?jXG)} zB^6Js`!-Jb#J-(zibPGSK0Raq$Wig~rfzp|!7Ok|!qUm!u3%V};8LZw_5@mcJo&Ib zu(Gz2}`@(T^wVG%i=R1$z3>gnhL{|Ny%ZE zOof0jC0Xp*cG21K(5_X7WjdEC{Wvp-d0_)FThKN5I(;Vhd4MgA;%tkSP=j@>eGk@VX#apNb_uaR(iSOLYH2#BQ#FiI;Z>mtyX3(+m; zHgq?791zEgfHPi0Z=ko)Cx9{zVI^P+1E2{jAcSo%h} zk?+VCULYytT67Nh@0z9E9g5Hm@oMKf@i?tiX+x6 z4GN<6)>Q{wD$_8^DloWqpTBn~&3+M{^r7%pRxEL28@djFN`>F=24}JjEs-d8pq*RU zXo+GMx?$b`w?x8zw2PAt=w@^iH>3Me-VLCJw}K+xj_yErqPsvL??LyX`&c;}!^X04 zY&<)Woy1ON6WBynaRYh)?M4rxhtR`l4|)XcMUSG#(BrI^}xOb^?D~0(!zfk4t>~{OT0biHsgPVb25l|tWGm>XkM!cM8?7bbr z?4h0NYN55Ky&Zf!_^}|@X@Ee3rVuli=l~UxLX7j1HW1=%y;*6 z1jVtX6Ve!&6k5IAfQ@Y}V9?pqyM?a0E-)LtTJjgq& zj!kEyqXWE;K1M11=mYd2`iM z+3drWMUo6~77(nc6(DY@Z8C@hj1C&x76#@9&o4LS9s<;3Xxri$Qk*P%q z!r>d$BIpz~2e?(;3sAz_7Htukgl#C?hqKw~toSvYi%-CLI3Fpo0~g>zTm+{@37i+D z@T(k;0m$r$L>!VWf&b*w)r+Bk32=|VQ9gIgjzG{4-uGyuPzJc+4*Fb|Dy>Tdo{^D` zrdJQ^<@1~tT|sl>RCqi3H0b39{5=aYKSRvTdf=Cj1rFkIaO|B3n>z^(zX^CE9DtR$ z3ReTOqJh*@Uz><(<_&7FWO14SklE<-1p&$lQaila>GpOFlJl^F8|w7=(J=@6*aFtZ z7P2nZ%GzD3f`OxA7&D~-2SM$?p&^ND5*OEG=3&JL@F{pI>}DF;ji=8ZoTXWup9!EG z^N){^MCXnhu@is{Skd4K4)gJ2Q*t(5e;7CMGx0BJl2-j8{T;wFU_mqSEIb>R43*TZ zptlp$+uhmCyzC5yT&j6oTgDX>6mV}HD1b1ct4;6&iu0aXQ0-gN)!}n{0tLr`SjTI3 zEaeeGx8HY$w7q%Q1(pduCn}4me8c#3zN=$(0qn)CxD9(|bG#{WgLSeFmuklU>RQ|J zg41B7QPt|lUVH{xGPDUSxB=XX;NhxQ1A%a_w95|M=~Cr@5Qpi}phSloRTy{it^Z^C z1GpPFvIzV@00((eqtzV{PJ?g1&;hoxSO6Pa#Jc}W5?z9qHrG4B_QQAy-$rViuhZe~ zhND!Hy@LxMB0!g_Trw~5v!p?4b@p_3`}{$NsL;TT5qcP6s}uavSd!yG<$gJHai zFNtSp{?sQKZwL+~yqYZmY=zeVSJz^I)Y`TlKz3chKfS}2vJJ=f$>-zsQJ*Y&xmB$9 z*k0~pd`ZO1ZDuRknGxr+LR!rh?nJC_3$fMx_(psazL}lHE?^h(Q!8&6*QL!z&#b6qqUH*q>saJ~iWhr*J%J{-zg>v% zuZm~bAnrKE=q{;kD7D0hJ2@lnVrR4Bm+`&ezPJ;+58sa;z`JKjfhrGl&tYrX8V13` z|FY|{Na63niYPPp;71`iz>na)Y#lo{j32{~v-8;b%-$k#twq`q0D(Vm@b9$wS^S(! zWf;b+qnY22pT{q7#r|uuV|&wME-JdQ{rCXNgN%n=@?!__t5V)^m?el%DvV#_E8t1V zKX-B*7=czUsdxBM|0e!d#0cJDm$8c@MzBee{d-*Y@3Tt=jo>5E2rm84jo=Xe4iZfG z8+;fa!QZmW*%o#MyK)=;9{+%U#6PjCSRdQPZeaGAQ-%4_m;-h=0LBO6hS^eVz~h)% zjo$VJB5{O^1d$k+hG0SnB_r6?>>740+qw;q@@|MDZGc{@AeK~en@ekfFo{RKfXIa< z9D4>D-AkmTEo6Cy_eL@_&Ng2+gj&X7o#N}@Nvj1&-FUkJ@`BB!3?mUo=TI=ZP8CUn z(1WO1xQ`^U{=W!4NGgOLL`!sR8{07e-bp$!9vu)66S5HtyAHOuotX#tXqmWbh;V#$X3h54HLz8RiK?>KA)75lOe56%1AjGL&lPEWIQ>MoWyQq zH?f=9E$miy8@rv|!PDyepC{JEf8ad-=ZSUcA0%)Bmv1x;;DK~@z}F%0&}FK()z4$f zE-oGqC_RsFd3ODn)J;)1HJm>8c=Qc*)lJYhZ1B9}5l<6>ZV%x2;p7}J9D6Mw(t0l9ZqW zqzQuHdS?Xk1{X|)45FCe9?~Y-2)kQ!RHTK>001;VA0{&as10d|>dkC&n&jW24f9AR z%Db7k$mzsQT1gx65P{4m?PLM*k~7Fc(!uUy_p=AsZuTI1h&{~qut(Tl_9%OdJ#RZ$a;x68GeTVsdHs?e(8=0mXIV#xM=|xgJ%-Y>@Xi8`%r&MfOrZxtZKTZe{z~8|=;h zTVRsh4S<5&!ww7qljMH#0K~+**(>a2z{%?)2=C@UqP*WjIKm^3kiG06do@fRBagG! z*z5lx!uu=iMV=$i5BE(kkykjE_mczUW%d^PH+#FE93-!j*VsGkWA@4aF)sg`bNOxd z?ockjN8TqN0GB^t@3Z$fQ2%czDVOCD?S{JRqO$yyd@&TQlP}3v>_heu=imPmK0hXT zbdqlgKueCCqYXcjpW$MU{KP)%BfqfEAq@*>D)(`4WJ5aV_+h|HNJ$a6D2_}hC?o;D!*=h9;la1RZMBhPR*U)d|YD89!l z4gNInaC$yLbWcWVhCNXe`@WA_*biJ>$GcqIPqSgSIn>UxAw01q>OhjFp_=_D@$m$j z2YlpLo?&_dS`wYZK@0gD1?=ZOTEu?&%Q;|%Qd&mKXTcl+QHJbSmug&PRZ&@CO>s^B zn5v@6{L!Op%JRpSl$PaJ6xY^NSB|bIEv+hbdAk?*x`cqYvj;LXaHH?`ca}-FIdm)? z2Z!&#eU79Z{m?vKyhxUwqw771P6Sn^C({Y+clJk^R?x~2Mj?#(%Kw9=w^*+uK=blN~qrH#}{n`kp_p)*35hVY0GjtSw|5RMDsks&-PgyTb4 z7QzX;kd4mfaGuVk^QeoSPTjPXDMMJq^dYPWVR%r5usVbjLpX_f4>X_k$1RH=~$WBXdIIB+)1FSBNH--)p z&60q}keY{ru2};w4|Gc_s)eh74nSC%p+oTqv!e<=KuM0)qGPn2JSfJasQ;06G>eJ} z(C?_1i^e#LTO_5JEqxW0=M_m#X0doR85M0WT?w%-J(Dh@%jt>`P6^@E5Y~pUZW~=i z84c0ZA*>JKv=B}YVFTCO6z~hf;aih8$mPWgA%=IfNacse>YCooiyuHZg@1Z0f*koZ zV^1f)CzOoJDYT1MJI$3n-VV>qqOz*e!jfXQ>N9Cy>(S1BdI7zVZV2Iw5VnS}ebA3y zOfQ48nO;IKrJF)HGlY#HZ0e_*>E(1)2%AIL0=wj!<4$k*9)?X0H;q2Oh`)Ip`WJ+i zkY{!ZaH%R)1lj+ZTAD))kt|Z|jL@mZsEqpPb;nx#cDjS^4B@O0wuRvS$!?i&NDRAC z8!~1QWZX}00v~uYy@h*O9*#~|kB}WzWfJ%QUn4qlRFfZWa-cv|rIRHjC8rzgxrO7# zpJ+L$qHfA5Q>Qs+%$(zjl=aCKP%4rLbvBWWO0_spiBk~-&)nJ*6asMSX&Ig6?16F@ z!6SGMXi^lZx-`D_g~N*g`?PFCpB`%RlHiD_h3MsAMyAnZR#>cAw(J~e;}u~r*ZkTJ zce_~bGmv9}QbMS;6YHw@F%74?Wui@DntGjz#f^kIzf{n0M+HxaJH<%5xsK0K32ya( z;)J|>M**B`(!7JYn*n9kAKf!#gDHtnSvrD^I)xxai0y$PB-$uRb6s=5-9GsC{uIT@6Zf+(cBH6)_9Copq@J~q8s74jqrFn zRNy5J(KbjAaZnd-h1ZYK4&~2kq=y`o9g3XGQ5_UEp9+O_KB#V44yDWM(H6)HUJXUd z*F&Yoy-;%TIFu~!gL38Ppg{RuxXu3t{fBHWbDg%acCIE2r_ z=iv+SMtm{86v~XRfI{PIpw#$oC?ozB{{+Ruzd_+}EExsG!*T*yKavck!=s@swBN0C`xLRo}-@>{6>xh zwr-=h&m90XfVtfL^fr12S^_NOPf<5?7u^k4|KLOKq4&~F^nUt)=t*-!_ylmKA)FV& z`P=D(_(%FM-2?gAA4Ax|c7s5ERWd`6OOq%W?#%(F^6J)kCnwH#wN$6#A;(RV@ z*eS8|V!LBk#IB897kggp`q&F&H^**?y)yRd*lT0^V(*Q8Hul3f6eo{U#3|#{ar(Hd zI7i(0xQTIfah|wEalyF7alLWN;#S069@iK5P~4uly`#dTZW#6Qs5j$B#CzlY@$2F@ z#NQtOaQxo*$Ks!i-xvQ({E_%yWus&YnM#%@OO@$lX)=Rsl5C+YDC?0ekulji*#)x8 zWSeDMWMSEDvU_EZ$ex#dDEnCUsqAyvm$I*A-^h;0zLWhR`$_goLSjO4LP~-*L7$MG zkda_aFeg|OYza9Dxd~$v<|iyoI4fau!Yv7}Bz!Jc$cyAn@)r3_d7E62`{h0ICGs=n z%jGNO7t61dZAMJDwtxm;%voQ#X7}#ip`2GiYpaYE3Q@a zDf$)HDQ;Cfrg%f~iQ-#jvNANH>g|GGu5-zbJUB~tJK%1`_+5Z&!}Hhf1v(KeMo&+{cU1% z;=IIQVsGMx#LE-=6R%6$k%W`tlTwllN%o}iN!3Zdq+rsuNjE0lnY1TqZ_;B)Pb6n2 z7bce_k56t+-jIBK^6km{lAllhF!`(GZ<3EBf0z8bMyE;B7&IqqCTc1*)tXw(WX%-K zR8766UE|d()O2cmnnjv`X1QjSCZst_vr%)g=2FdNnp-vZX`awLrFmNOtmbviJDT@2 zA80<(e5v_b^Nr?+=DQSYiZf+-%9@mQDd(lEPq{i}N6PgnH>BK@a!aZzRhMc@Js~we zwIFqLYFX--)N!d5sgqOZrY=cksasQbq&}JYR_fu@KeR}TwN#s+wQB9!3awMyq3zOk zYyH}wcCmJ;Hl#gUyH>kSyGgrQyG47Y_Ac#1+Gn)SX%6*iblY@K>fX_PsXL@Q ztou&)gYGB2T%V#((;M`edXwIvFV&ap$Lh!HEA`d-TK#1G6unbFNAJ~l>;3wmezAUq zew99?KTE$xzd?Vw{tEpL{k{78^}F>C>G$aO>L1fTp?^yMy8cc5Tl%;4@9N*zf2jXh z|Ed1-G+kO<+Tyh9(q2qQ>BZ^p^u_6CrEgB(m40LTUFnacKasva{q6L3(%(&gKmCwF zZcrN3h9rZ=kZRBw(hLSerom*e7_to62D_owFxfE0Fx60RXfQMynhY(5nTFF0a}B2( z+6;oB-LTqlt>H$)vxXxXF&SwYV=|^@v}LTw*pRU|;*0Z$8O9 z!Q5bOG&h-B%rngonV&SjY<}JRrui-Nd*%4 zTRydXZu!#kt>t^mkCvY;zgi8}X6s7p`PK`q8?BdEH(57ZZ?oQS-D`cu`keKk^)2gr z)~~IHt>0RIu>NHIB}SJq)$ zk!_r<%2s2mx6QE4vdyXV+!V%U+PZB70Nzj_mugf6hTUDLICmoSdSZ;+)Yr z6**IKrsXu`ICENZTsf^dLe7Gmg*lx$YjgT?cISMZ^IgvGc4QxASJ{*7DR!Mb&7NZ~ zuov4)?PKf{?3MN!`(*nRd%)gjf6ji;{+j(m`xo}F?T78(*?+bFkxO!8a^rGUxv9Cv z+#E-_W1OSPajIjc!{z93^g5P1RykHX);QKV&UakyxYBX8W2>Xzah+p_<9f&Qjw1zz cf=LAp1+xlT3p^3jL!@u|Toms}-vxpH0g)CFApigX From 28b67729aea8bf52b472efa8ba9c130e60f07278 Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 14:17:34 -0300 Subject: [PATCH 03/13] Removing unnecessary destinations, leaving only iPhone. Disabling unnecessary orientations, leaving only portrait. --- .../project.pbxproj | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/busbud-coding-challenge.xcodeproj/project.pbxproj b/busbud-coding-challenge.xcodeproj/project.pbxproj index 97af442..0530d27 100644 --- a/busbud-coding-challenge.xcodeproj/project.pbxproj +++ b/busbud-coding-challenge.xcodeproj/project.pbxproj @@ -294,8 +294,8 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -303,9 +303,13 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challenge"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -321,8 +325,8 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -330,9 +334,13 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challenge"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; From b4e7c4c7875921c7042b4cdcf6ddf1d48d4e3f4a Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 16:42:31 -0300 Subject: [PATCH 04/13] - Setup basic navigation. - Created service and models. --- .../project.pbxproj | 2 - .../busbud-coding-challenge.xcscheme | 102 +++++++++++++++++ .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 18 +++ busbud-coding-challenge/AppDelegate.swift | 7 -- .../Base.lproj/Main.storyboard | 24 ---- busbud-coding-challenge/Info.plist | 2 - busbud-coding-challenge/SceneDelegate.swift | 19 ++-- .../Service/BusbudService.swift | 36 ++++++ .../Service/ServiceModels.swift | 79 +++++++++++++ busbud-coding-challenge/ViewController.swift | 19 ---- .../ViewControllers/HomeViewController.swift | 105 ++++++++++++++++++ .../SuggestionsListViewController.swift | 31 ++++++ 13 files changed, 388 insertions(+), 62 deletions(-) create mode 100644 busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme create mode 100644 busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist delete mode 100644 busbud-coding-challenge/Base.lproj/Main.storyboard create mode 100644 busbud-coding-challenge/Service/BusbudService.swift create mode 100644 busbud-coding-challenge/Service/ServiceModels.swift delete mode 100644 busbud-coding-challenge/ViewController.swift create mode 100644 busbud-coding-challenge/ViewControllers/HomeViewController.swift create mode 100644 busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift diff --git a/busbud-coding-challenge.xcodeproj/project.pbxproj b/busbud-coding-challenge.xcodeproj/project.pbxproj index 0530d27..e90e232 100644 --- a/busbud-coding-challenge.xcodeproj/project.pbxproj +++ b/busbud-coding-challenge.xcodeproj/project.pbxproj @@ -293,7 +293,6 @@ INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( @@ -324,7 +323,6 @@ INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme new file mode 100644 index 0000000..dce3eba --- /dev/null +++ b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..1342c69 --- /dev/null +++ b/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist b/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist index bbe2434..916c58d 100644 --- a/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/busbud-coding-challenge.xcodeproj/xcuserdata/spencerdiniz.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,23 @@ 0 + SuppressBuildableAutocreation + + 14BBD44B2D11E6240047FE7A + + primary + + + 14BBD4612D11E6250047FE7A + + primary + + + 14BBD46B2D11E6250047FE7A + + primary + + + diff --git a/busbud-coding-challenge/AppDelegate.swift b/busbud-coding-challenge/AppDelegate.swift index 5399603..0446958 100644 --- a/busbud-coding-challenge/AppDelegate.swift +++ b/busbud-coding-challenge/AppDelegate.swift @@ -9,16 +9,11 @@ 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. @@ -30,7 +25,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // 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/busbud-coding-challenge/Base.lproj/Main.storyboard b/busbud-coding-challenge/Base.lproj/Main.storyboard deleted file mode 100644 index 25a7638..0000000 --- a/busbud-coding-challenge/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/busbud-coding-challenge/Info.plist b/busbud-coding-challenge/Info.plist index dd3c9af..0eb786d 100644 --- a/busbud-coding-challenge/Info.plist +++ b/busbud-coding-challenge/Info.plist @@ -15,8 +15,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/busbud-coding-challenge/SceneDelegate.swift b/busbud-coding-challenge/SceneDelegate.swift index 8eb4a02..93d29ec 100644 --- a/busbud-coding-challenge/SceneDelegate.swift +++ b/busbud-coding-challenge/SceneDelegate.swift @@ -8,15 +8,20 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: windowScene) + window.backgroundColor = .white + self.window = window + + let homeViewController = HomeViewController() + let navigationController = UINavigationController(rootViewController: homeViewController) + + window.rootViewController = navigationController + window.makeKeyAndVisible() } func sceneDidDisconnect(_ scene: UIScene) { @@ -46,7 +51,5 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 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/busbud-coding-challenge/Service/BusbudService.swift b/busbud-coding-challenge/Service/BusbudService.swift new file mode 100644 index 0000000..efad812 --- /dev/null +++ b/busbud-coding-challenge/Service/BusbudService.swift @@ -0,0 +1,36 @@ +// +// BusbudService.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import Foundation + +public class BusbudService { + public static let shared = BusbudService() + + func fetchSuggestions() async throws -> [Suggestion] { + let url = URL(string: "https://napi.busbud.com/flex/suggestions/points-of-interest")! + var request = URLRequest(url: url) + request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData + request.httpMethod = "GET" + request.addValue("application/vnd.busbud+json; version=3; profile=https://schema.busbud.com/v3/anything.json", forHTTPHeaderField: "Accept") + + do { + let (data, response) = try await URLSession.shared.data(for: request) + + //TODO: Check response codes and handle accordingly. + if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) { + throw URLError(.badServerResponse) + } + + let decoder = JSONDecoder() + let suggestionResponse = try decoder.decode(SuggestionsResponse.self, from: data) + return suggestionResponse.suggestions + } catch { + print("Failed to fetch suggestions: \(error)") + throw error + } + } +} diff --git a/busbud-coding-challenge/Service/ServiceModels.swift b/busbud-coding-challenge/Service/ServiceModels.swift new file mode 100644 index 0000000..9485498 --- /dev/null +++ b/busbud-coding-challenge/Service/ServiceModels.swift @@ -0,0 +1,79 @@ +// +// ServiceModels.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import Foundation + +struct SuggestionsResponse: Codable { + let suggestions: [Suggestion] + let resultsType: String + + enum CodingKeys: String, CodingKey { + case suggestions + case resultsType = "results_type" + } +} + +struct Suggestion: Codable { + let provider: String + let id: String + let placeID: String + let geoEntityID: String + let parentID: String? + let label: String + let score: Double + let placeType: String + let stopType: String + let url: String + let geohash: String + let lat: Double + let lon: Double + let hierarchyInfo: HierarchyInfo + let cityName: String + let regionName: String + let countryName: String + let fullName: String + let shortName: String + let requestID: String + + enum CodingKeys: String, CodingKey { + case provider + case id + case placeID = "place_id" + case geoEntityID = "geo_entity_id" + case parentID = "parent_id" + case label + case score + case placeType = "place_type" + case stopType = "stop_type" + case url + case geohash + case lat + case lon + case hierarchyInfo = "hierarchy_info" + case cityName = "city_name" + case regionName = "region_name" + case countryName = "country_name" + case fullName = "full_name" + case shortName = "short_name" + case requestID = "request_id" + } +} + +struct HierarchyInfo: Codable { + let country: Country + let region: Region +} + +struct Country: Codable { + let code: String? + let name: String +} + +struct Region: Codable { + let code: String? + let name: String +} diff --git a/busbud-coding-challenge/ViewController.swift b/busbud-coding-challenge/ViewController.swift deleted file mode 100644 index 49c45ff..0000000 --- a/busbud-coding-challenge/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// busbud-coding-challenge -// -// Created by Spencer Diniz on 17/12/24. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/busbud-coding-challenge/ViewControllers/HomeViewController.swift b/busbud-coding-challenge/ViewControllers/HomeViewController.swift new file mode 100644 index 0000000..bc95477 --- /dev/null +++ b/busbud-coding-challenge/ViewControllers/HomeViewController.swift @@ -0,0 +1,105 @@ +// +// HomeViewController.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import UIKit + +class HomeViewController: UIViewController { + private var stackViewMain: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 20 + stackView.distribution = .fillEqually + + return stackView + }() + + private var stackViewButtons: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.spacing = 20 + stackView.distribution = .fillEqually + + return stackView + }() + + private var labelTitle: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Choose implmentation:" + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 20) + + return label + }() + + private var buttonUIKit: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("UIKit", for: .normal) + button.setTitleColor(.white, for: .normal) + button.backgroundColor = .systemBlue + button.clipsToBounds = true + button.layer.cornerRadius = 22.0 + button.layer.masksToBounds = true + + return button + }() + + private var buttonSwiftUI: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("SwiftUI", for: .normal) + button.setTitleColor(.white, for: .normal) + button.backgroundColor = .systemBlue + button.clipsToBounds = true + button.layer.cornerRadius = 22.0 + button.layer.masksToBounds = true + + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Home" + setupUI() + } + + private func setupUI() { + view.backgroundColor = .white + + view.addSubview(stackViewMain) + + NSLayoutConstraint.activate([ + stackViewMain.centerXAnchor.constraint(equalTo: view.centerXAnchor), + stackViewMain.centerYAnchor.constraint(equalTo: view.centerYAnchor), + buttonUIKit.heightAnchor.constraint(equalToConstant: 44), + buttonUIKit.widthAnchor.constraint(equalToConstant: 120), + buttonSwiftUI.heightAnchor.constraint(equalToConstant: 44), + buttonSwiftUI.widthAnchor.constraint(equalToConstant: 120), + ]) + + stackViewMain.addArrangedSubview(labelTitle) + stackViewMain.addArrangedSubview(stackViewButtons) + stackViewButtons.addArrangedSubview(buttonUIKit) + stackViewButtons.addArrangedSubview(buttonSwiftUI) + + buttonUIKit.addTarget(self, action: #selector(startUIKitDemo), for: .touchUpInside) + buttonSwiftUI.addTarget(self, action: #selector(startSwiftUIDemo), for: .touchUpInside) + } + + @objc private func startUIKitDemo() { + let suggestionsViewController = SuggestionsListViewController() + navigationController?.pushViewController(suggestionsViewController, animated: true) + } + + @objc private func startSwiftUIDemo() { + print("SwiftUI Demo") + } +} diff --git a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift new file mode 100644 index 0000000..ad20d63 --- /dev/null +++ b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift @@ -0,0 +1,31 @@ +// +// SuggestionsViewController.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import UIKit + +class SuggestionsListViewController: UITableViewController { + override func viewDidLoad() { + super.viewDidLoad() + + title = "Suggestions" + view.backgroundColor = .white + + Task { + await loadData() + } + + print("VIEW DID LOAD ") + } + + private func loadData() async { + let suggestions = try? await BusbudService.shared.fetchSuggestions() + + DispatchQueue.main.async { + print(suggestions ?? []) + } + } +} From 854192b362d7aae6c028d9a9c44883f5b2b77d78 Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 18:03:22 -0300 Subject: [PATCH 05/13] - Fixed crash with location services. - Calculated distance for each suggestion. - Format distance in KM and Miles. - Update list UI to show distance. --- .../project.pbxproj | 2 + .../Service/BusbudService.swift | 17 ++++- .../Service/LocationService.swift | 75 +++++++++++++++++++ .../Service/ServiceModels.swift | 12 +++ .../SuggestionsListViewController.swift | 44 +++++++++-- 5 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 busbud-coding-challenge/Service/LocationService.swift diff --git a/busbud-coding-challenge.xcodeproj/project.pbxproj b/busbud-coding-challenge.xcodeproj/project.pbxproj index e90e232..f27d7f0 100644 --- a/busbud-coding-challenge.xcodeproj/project.pbxproj +++ b/busbud-coding-challenge.xcodeproj/project.pbxproj @@ -291,6 +291,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "We need your location even when the app is in the background."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; @@ -321,6 +322,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "We need your location even when the app is in the background."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; diff --git a/busbud-coding-challenge/Service/BusbudService.swift b/busbud-coding-challenge/Service/BusbudService.swift index efad812..e32d30f 100644 --- a/busbud-coding-challenge/Service/BusbudService.swift +++ b/busbud-coding-challenge/Service/BusbudService.swift @@ -6,12 +6,13 @@ // import Foundation +import CoreLocation public class BusbudService { public static let shared = BusbudService() - func fetchSuggestions() async throws -> [Suggestion] { - let url = URL(string: "https://napi.busbud.com/flex/suggestions/points-of-interest")! + func fetchSuggestions(for coordinate: CLLocationCoordinate2D) async throws -> [Suggestion] { + let url = URL(string: "https://napi.busbud.com/flex/suggestions/points-of-interest?lang=en&limit=100&lat=\(coordinate.latitude)&lon=\(coordinate.longitude)")! var request = URLRequest(url: url) request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData request.httpMethod = "GET" @@ -27,7 +28,17 @@ public class BusbudService { let decoder = JSONDecoder() let suggestionResponse = try decoder.decode(SuggestionsResponse.self, from: data) - return suggestionResponse.suggestions + var suggestionsWithDistance: [Suggestion] = [] + + for var suggestion in suggestionResponse.suggestions { + let suggestionLocation = CLLocation(latitude: suggestion.lat, longitude: suggestion.lon) + let distance = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude).distance(from: suggestionLocation) + suggestion.distance = distance + suggestionsWithDistance.append(suggestion) + } + + let sortedSuggestions = suggestionsWithDistance.sorted { $0.distance ?? 0 < $1.distance ?? 0 } + return sortedSuggestions } catch { print("Failed to fetch suggestions: \(error)") throw error diff --git a/busbud-coding-challenge/Service/LocationService.swift b/busbud-coding-challenge/Service/LocationService.swift new file mode 100644 index 0000000..51a9c4d --- /dev/null +++ b/busbud-coding-challenge/Service/LocationService.swift @@ -0,0 +1,75 @@ +// +// LocationService.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import Foundation +import CoreLocation + +class LocationService: NSObject, CLLocationManagerDelegate { + private let locationManager = CLLocationManager() + private var locationContinuation: CheckedContinuation? + private var permissionContinuation: CheckedContinuation? + + func requestLocation() async throws -> CLLocationCoordinate2D { + try await requestPermission() // Ensure permission is granted + + return try await withCheckedThrowingContinuation { continuation in + self.locationContinuation = continuation + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.startUpdatingLocation() + } + } + + private func requestPermission() async throws { + return try await withCheckedThrowingContinuation { continuation in + self.permissionContinuation = continuation + + locationManager.delegate = self + let status = locationManager.authorizationStatus + + switch status { + case .notDetermined: + locationManager.requestWhenInUseAuthorization() + default: + break + } + } + } + + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + switch status { + case .authorizedWhenInUse, .authorizedAlways: + permissionContinuation?.resume() + permissionContinuation = nil + case .restricted, .denied: + permissionContinuation?.resume(throwing: LocationError.authorizationDenied) + permissionContinuation = nil + default: + break + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last else { return } + let coordinate = location.coordinate + + locationContinuation?.resume(returning: coordinate) + locationContinuation = nil + locationManager.stopUpdatingLocation() + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + locationContinuation?.resume(throwing: error) + locationContinuation = nil + locationManager.stopUpdatingLocation() + } + + enum LocationError: Error { + case authorizationDenied + case unknown + } +} diff --git a/busbud-coding-challenge/Service/ServiceModels.swift b/busbud-coding-challenge/Service/ServiceModels.swift index 9485498..5dcf42c 100644 --- a/busbud-coding-challenge/Service/ServiceModels.swift +++ b/busbud-coding-challenge/Service/ServiceModels.swift @@ -6,6 +6,7 @@ // import Foundation +import CoreLocation struct SuggestionsResponse: Codable { let suggestions: [Suggestion] @@ -38,6 +39,7 @@ struct Suggestion: Codable { let fullName: String let shortName: String let requestID: String + var distance: Double? enum CodingKeys: String, CodingKey { case provider @@ -61,6 +63,16 @@ struct Suggestion: Codable { case shortName = "short_name" case requestID = "request_id" } + + var formattedDistanceInKm: String { + guard let distance = distance else { return "" } + return String(format: "%.2f km", distance / 1000) + } + + var formattedDistanceInMiles: String { + guard let distance = distance else { return "" } + return String(format: "%.2f mi", distance / 1609.34) + } } struct HierarchyInfo: Codable { diff --git a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift index ad20d63..9b5e523 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift @@ -6,26 +6,58 @@ // import UIKit +import CoreLocation class SuggestionsListViewController: UITableViewController { + private var suggestions: [Suggestion] = [] { + didSet { + tableView.reloadData() + } + } + override func viewDidLoad() { super.viewDidLoad() title = "Suggestions" - view.backgroundColor = .white Task { - await loadData() + do { + let locationService = LocationService() + let coordinate = try await locationService.requestLocation() + await loadData(for: coordinate) + } catch { + print("Failed to get location: \(error.localizedDescription)") + } } + } - print("VIEW DID LOAD ") + private func setupUI() { + view.backgroundColor = .white + tableView.separatorInset = .zero } - private func loadData() async { - let suggestions = try? await BusbudService.shared.fetchSuggestions() + private func loadData(for coordinate: CLLocationCoordinate2D) async { + let suggestions = try? await BusbudService.shared.fetchSuggestions(for: coordinate) DispatchQueue.main.async { - print(suggestions ?? []) + self.suggestions = suggestions ?? [] } } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return suggestions.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) + let suggestion = suggestions[indexPath.row] + + cell.textLabel?.text = suggestion.cityName + cell.detailTextLabel?.text = "\(suggestion.formattedDistanceInKm) / \(suggestion.formattedDistanceInMiles)" + cell.accessoryType = .disclosureIndicator + cell.separatorInset = .zero + cell.selectionStyle = .none + + return cell + } } From fbc01427626298e95630f901b270b8b0e0ccd198 Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 19:29:52 -0300 Subject: [PATCH 06/13] - Added Detail View Controller; - Support for light/dark modes using sytem colors; --- .../project.pbxproj | 2 + .../Service/LocationService.swift | 5 +- .../Service/ServiceModels.swift | 14 +- .../ViewControllers/HomeViewController.swift | 22 +- .../SuggestionDetailViewController.swift | 225 ++++++++++++++++++ .../SuggestionsListViewController.swift | 13 +- 6 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift diff --git a/busbud-coding-challenge.xcodeproj/project.pbxproj b/busbud-coding-challenge.xcodeproj/project.pbxproj index f27d7f0..c9fa5c3 100644 --- a/busbud-coding-challenge.xcodeproj/project.pbxproj +++ b/busbud-coding-challenge.xcodeproj/project.pbxproj @@ -291,6 +291,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Busbud; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "We need your location even when the app is in the background."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -322,6 +323,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "busbud-coding-challenge/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Busbud; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "We need your location even when the app is in the background."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; diff --git a/busbud-coding-challenge/Service/LocationService.swift b/busbud-coding-challenge/Service/LocationService.swift index 51a9c4d..4b3520a 100644 --- a/busbud-coding-challenge/Service/LocationService.swift +++ b/busbud-coding-challenge/Service/LocationService.swift @@ -54,7 +54,10 @@ class LocationService: NSObject, CLLocationManagerDelegate { } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last else { return } + guard let location = locations.last else { + return + } + let coordinate = location.coordinate locationContinuation?.resume(returning: coordinate) diff --git a/busbud-coding-challenge/Service/ServiceModels.swift b/busbud-coding-challenge/Service/ServiceModels.swift index 5dcf42c..b61c4ce 100644 --- a/busbud-coding-challenge/Service/ServiceModels.swift +++ b/busbud-coding-challenge/Service/ServiceModels.swift @@ -65,14 +65,24 @@ struct Suggestion: Codable { } var formattedDistanceInKm: String { - guard let distance = distance else { return "" } + guard let distance = distance else { + return "" + } + return String(format: "%.2f km", distance / 1000) } var formattedDistanceInMiles: String { - guard let distance = distance else { return "" } + guard let distance = distance else { + return "" + } + return String(format: "%.2f mi", distance / 1609.34) } + + var formattedDistance: String { + return "\(formattedDistanceInKm) / \(formattedDistanceInMiles)" + } } struct HierarchyInfo: Codable { diff --git a/busbud-coding-challenge/ViewControllers/HomeViewController.swift b/busbud-coding-challenge/ViewControllers/HomeViewController.swift index bc95477..043b8cd 100644 --- a/busbud-coding-challenge/ViewControllers/HomeViewController.swift +++ b/busbud-coding-challenge/ViewControllers/HomeViewController.swift @@ -42,10 +42,10 @@ class HomeViewController: UIViewController { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("UIKit", for: .normal) - button.setTitleColor(.white, for: .normal) + button.setTitleColor(.lightText, for: .normal) button.backgroundColor = .systemBlue button.clipsToBounds = true - button.layer.cornerRadius = 22.0 + button.layer.cornerRadius = 4.0 button.layer.masksToBounds = true return button @@ -55,10 +55,10 @@ class HomeViewController: UIViewController { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("SwiftUI", for: .normal) - button.setTitleColor(.white, for: .normal) + button.setTitleColor(.lightText, for: .normal) button.backgroundColor = .systemBlue button.clipsToBounds = true - button.layer.cornerRadius = 22.0 + button.layer.cornerRadius = 4.0 button.layer.masksToBounds = true return button @@ -68,14 +68,21 @@ class HomeViewController: UIViewController { super.viewDidLoad() title = "Home" + navigationItem.backButtonTitle = "" + setupUI() } private func setupUI() { - view.backgroundColor = .white + view.backgroundColor = .systemBackground view.addSubview(stackViewMain) + stackViewMain.addArrangedSubview(labelTitle) + stackViewMain.addArrangedSubview(stackViewButtons) + stackViewButtons.addArrangedSubview(buttonUIKit) + stackViewButtons.addArrangedSubview(buttonSwiftUI) + NSLayoutConstraint.activate([ stackViewMain.centerXAnchor.constraint(equalTo: view.centerXAnchor), stackViewMain.centerYAnchor.constraint(equalTo: view.centerYAnchor), @@ -85,11 +92,6 @@ class HomeViewController: UIViewController { buttonSwiftUI.widthAnchor.constraint(equalToConstant: 120), ]) - stackViewMain.addArrangedSubview(labelTitle) - stackViewMain.addArrangedSubview(stackViewButtons) - stackViewButtons.addArrangedSubview(buttonUIKit) - stackViewButtons.addArrangedSubview(buttonSwiftUI) - buttonUIKit.addTarget(self, action: #selector(startUIKitDemo), for: .touchUpInside) buttonSwiftUI.addTarget(self, action: #selector(startSwiftUIDemo), for: .touchUpInside) } diff --git a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift new file mode 100644 index 0000000..7f4482a --- /dev/null +++ b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift @@ -0,0 +1,225 @@ +// +// SuggestionDetailViewController.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import UIKit +import MapKit + +class SuggestionDetailViewController: UIViewController { + let scrollViewMain: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.alwaysBounceVertical = true + + return scrollView + }() + + let stackViewMain: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 0.0 + stackView.distribution = .fill + + return stackView + }() + + let labelCity: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 16) + label.text = "City" + label.textColor = .secondaryLabel + + return label + }() + + let labelCityValue: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .boldSystemFont(ofSize: 16) + label.textColor = .label + + return label + }() + + let labelRegion: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 16) + label.text = "State / Province / Region" + label.textColor = .secondaryLabel + + return label + }() + + let labelRegionValue: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .boldSystemFont(ofSize: 16) + label.textColor = .label + + return label + }() + + let labelCountry: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 16) + label.text = "Country" + label.textColor = .secondaryLabel + + return label + }() + + let labelCountryValue: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .boldSystemFont(ofSize: 16) + label.textColor = .label + + return label + }() + + let labelDistance: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 16) + label.text = "Distance" + label.textColor = .secondaryLabel + + return label + }() + + let labelDistanceValue: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .boldSystemFont(ofSize: 16) + label.textColor = .label + + return label + }() + + let mapView: MKMapView = { + let mapView = MKMapView() + mapView.translatesAutoresizingMaskIntoConstraints = false + mapView.clipsToBounds = true + mapView.layer.cornerRadius = 4 + mapView.layer.masksToBounds = true + mapView.layer.borderWidth = 1 + mapView.layer.borderColor = UIColor.opaqueSeparator.cgColor + + return mapView + }() + + let buttonGoToWebsite: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Go to Busbud Website", for: .normal) + button.setTitleColor(.lightText, for: .normal) + button.backgroundColor = .systemBlue + button.clipsToBounds = true + button.layer.cornerRadius = 4.0 + button.layer.masksToBounds = true + + return button + }() + + let suggestion: Suggestion + + init(suggestion: Suggestion) { + self.suggestion = suggestion + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Details" + navigationItem.backButtonTitle = "" + + setupUI() + loadInfo() + } + + private func setupUI() { + view.backgroundColor = .systemBackground + + view.addSubview(scrollViewMain) + scrollViewMain.addSubview(stackViewMain) + + stackViewMain.addArrangedSubview(labelCity) + stackViewMain.addArrangedSubview(labelCityValue) + stackViewMain.addArrangedSubview(labelRegion) + stackViewMain.addArrangedSubview(labelRegionValue) + stackViewMain.addArrangedSubview(labelCountry) + stackViewMain.addArrangedSubview(labelCountryValue) + stackViewMain.addArrangedSubview(labelDistance) + stackViewMain.addArrangedSubview(labelDistanceValue) + stackViewMain.addArrangedSubview(mapView) + stackViewMain.addArrangedSubview(buttonGoToWebsite) + + stackViewMain.setCustomSpacing(14.0, after: labelCityValue) + stackViewMain.setCustomSpacing(14.0, after: labelRegionValue) + stackViewMain.setCustomSpacing(14.0, after: labelCountryValue) + stackViewMain.setCustomSpacing(14.0, after: labelDistanceValue) + stackViewMain.setCustomSpacing(14.0, after: mapView) + + NSLayoutConstraint.activate([ + scrollViewMain.topAnchor.constraint(equalTo: view.topAnchor), + scrollViewMain.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollViewMain.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollViewMain.bottomAnchor.constraint(equalTo: view.bottomAnchor), + stackViewMain.topAnchor.constraint(equalTo: scrollViewMain.topAnchor), + stackViewMain.bottomAnchor.constraint(equalTo: scrollViewMain.bottomAnchor), + stackViewMain.leadingAnchor.constraint(equalTo: scrollViewMain.leadingAnchor, constant: 20.0), + stackViewMain.trailingAnchor.constraint(equalTo: scrollViewMain.trailingAnchor, constant: -20.0), + stackViewMain.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -40.0), + mapView.heightAnchor.constraint(equalTo: mapView.widthAnchor), + buttonGoToWebsite.heightAnchor.constraint(equalToConstant: 44.0), + ]) + + buttonGoToWebsite.addTarget(self, action: #selector(goToWebsite), for: .touchUpInside) + } + + private func loadInfo() { + labelCityValue.text = suggestion.cityName + labelRegionValue.text = suggestion.regionName + labelCountryValue.text = suggestion.countryName + labelDistanceValue.text = suggestion.formattedDistance + centerMapOnLocation() + } + + private func centerMapOnLocation() { + let coordinate = CLLocationCoordinate2D(latitude: suggestion.lat, longitude: suggestion.lon) + + let region = MKCoordinateRegion( + center: coordinate, + latitudinalMeters: 10000, + longitudinalMeters: 10000 + ) + + let annotation = MKPointAnnotation() + annotation.coordinate = coordinate + annotation.title = suggestion.cityName + + mapView.addAnnotation(annotation) + mapView.setRegion(region, animated: true) + } + + @objc private func goToWebsite() { + guard let url = URL(string: "https://www.busbud.com/en/c/\(suggestion.geohash)") else { + return + } + + UIApplication.shared.open(url) + } +} diff --git a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift index 9b5e523..b60d5ad 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift @@ -19,6 +19,9 @@ class SuggestionsListViewController: UITableViewController { super.viewDidLoad() title = "Suggestions" + navigationItem.backButtonTitle = "" + + setupUI() Task { do { @@ -32,7 +35,7 @@ class SuggestionsListViewController: UITableViewController { } private func setupUI() { - view.backgroundColor = .white + view.backgroundColor = .systemBackground tableView.separatorInset = .zero } @@ -53,11 +56,17 @@ class SuggestionsListViewController: UITableViewController { let suggestion = suggestions[indexPath.row] cell.textLabel?.text = suggestion.cityName - cell.detailTextLabel?.text = "\(suggestion.formattedDistanceInKm) / \(suggestion.formattedDistanceInMiles)" + cell.detailTextLabel?.text = suggestion.formattedDistance cell.accessoryType = .disclosureIndicator cell.separatorInset = .zero cell.selectionStyle = .none return cell } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let suggestion = suggestions[indexPath.row] + let suggestionDetailViewController = SuggestionDetailViewController(suggestion: suggestion) + navigationController?.pushViewController(suggestionDetailViewController, animated: true) + } } From d7e039fd3563e1f873614d84d4cf2188c0143166 Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 19:42:56 -0300 Subject: [PATCH 07/13] - Disabled scrolling in map view. - Disabled zooming in map view. --- .../ViewControllers/SuggestionDetailViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift index 7f4482a..d461ad0 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift @@ -106,6 +106,8 @@ class SuggestionDetailViewController: UIViewController { let mapView: MKMapView = { let mapView = MKMapView() mapView.translatesAutoresizingMaskIntoConstraints = false + mapView.isScrollEnabled = false + mapView.isZoomEnabled = false mapView.clipsToBounds = true mapView.layer.cornerRadius = 4 mapView.layer.masksToBounds = true From 327340ba2f1f58b812872d2798b864d85ce46bde Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 20:14:02 -0300 Subject: [PATCH 08/13] - Created view models for view controllers - Refactored view controllers to user view models --- .../ViewControllers/HomeViewController.swift | 12 +++--- .../SuggestionDetailViewController.swift | 29 +++++++------ .../SuggestionsListViewController.swift | 38 +++++++++-------- .../SuggestionDetailViewModel.swift | 41 +++++++++++++++++++ .../ViewModels/SuggestionsListViewModel.swift | 19 +++++++++ 5 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift create mode 100644 busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift diff --git a/busbud-coding-challenge/ViewControllers/HomeViewController.swift b/busbud-coding-challenge/ViewControllers/HomeViewController.swift index 043b8cd..4eb5717 100644 --- a/busbud-coding-challenge/ViewControllers/HomeViewController.swift +++ b/busbud-coding-challenge/ViewControllers/HomeViewController.swift @@ -42,8 +42,8 @@ class HomeViewController: UIViewController { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("UIKit", for: .normal) - button.setTitleColor(.lightText, for: .normal) - button.backgroundColor = .systemBlue + button.setTitleColor(.darkText, for: .normal) + button.backgroundColor = .systemOrange button.clipsToBounds = true button.layer.cornerRadius = 4.0 button.layer.masksToBounds = true @@ -55,8 +55,8 @@ class HomeViewController: UIViewController { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("SwiftUI", for: .normal) - button.setTitleColor(.lightText, for: .normal) - button.backgroundColor = .systemBlue + button.setTitleColor(.darkText, for: .normal) + button.backgroundColor = .systemOrange button.clipsToBounds = true button.layer.cornerRadius = 4.0 button.layer.masksToBounds = true @@ -97,7 +97,9 @@ class HomeViewController: UIViewController { } @objc private func startUIKitDemo() { - let suggestionsViewController = SuggestionsListViewController() + let suggestionsViewModel = SuggestionsListViewModel() + let suggestionsViewController = SuggestionsListViewController(viewModel: suggestionsViewModel) + navigationController?.pushViewController(suggestionsViewController, animated: true) } diff --git a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift index d461ad0..b181983 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift @@ -121,8 +121,8 @@ class SuggestionDetailViewController: UIViewController { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("Go to Busbud Website", for: .normal) - button.setTitleColor(.lightText, for: .normal) - button.backgroundColor = .systemBlue + button.setTitleColor(.darkText, for: .normal) + button.backgroundColor = .systemOrange button.clipsToBounds = true button.layer.cornerRadius = 4.0 button.layer.masksToBounds = true @@ -130,10 +130,10 @@ class SuggestionDetailViewController: UIViewController { return button }() - let suggestion: Suggestion + let viewModel: SuggestionDetailViewModel - init(suggestion: Suggestion) { - self.suggestion = suggestion + init(viewModel: SuggestionDetailViewModel) { + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -193,32 +193,31 @@ class SuggestionDetailViewController: UIViewController { } private func loadInfo() { - labelCityValue.text = suggestion.cityName - labelRegionValue.text = suggestion.regionName - labelCountryValue.text = suggestion.countryName - labelDistanceValue.text = suggestion.formattedDistance + labelCityValue.text = viewModel.city + labelRegionValue.text = viewModel.region + labelCountryValue.text = viewModel.country + labelDistanceValue.text = viewModel.distance + centerMapOnLocation() } private func centerMapOnLocation() { - let coordinate = CLLocationCoordinate2D(latitude: suggestion.lat, longitude: suggestion.lon) - let region = MKCoordinateRegion( - center: coordinate, + center: viewModel.coordinate, latitudinalMeters: 10000, longitudinalMeters: 10000 ) let annotation = MKPointAnnotation() - annotation.coordinate = coordinate - annotation.title = suggestion.cityName + annotation.coordinate = viewModel.coordinate + annotation.title = viewModel.city mapView.addAnnotation(annotation) mapView.setRegion(region, animated: true) } @objc private func goToWebsite() { - guard let url = URL(string: "https://www.busbud.com/en/c/\(suggestion.geohash)") else { + guard let url = URL(string: "https://www.busbud.com/en/c/\(viewModel.geohash)") else { return } diff --git a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift index b60d5ad..9cb4ae6 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift @@ -9,10 +9,16 @@ import UIKit import CoreLocation class SuggestionsListViewController: UITableViewController { - private var suggestions: [Suggestion] = [] { - didSet { - tableView.reloadData() - } + private let viewModel: SuggestionsListViewModel + + init(viewModel: SuggestionsListViewModel) { + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { @@ -27,7 +33,11 @@ class SuggestionsListViewController: UITableViewController { do { let locationService = LocationService() let coordinate = try await locationService.requestLocation() - await loadData(for: coordinate) + await viewModel.loadData(for: coordinate) + + DispatchQueue.main.async { [weak self] in + self?.tableView.reloadData() + } } catch { print("Failed to get location: \(error.localizedDescription)") } @@ -39,21 +49,13 @@ class SuggestionsListViewController: UITableViewController { tableView.separatorInset = .zero } - private func loadData(for coordinate: CLLocationCoordinate2D) async { - let suggestions = try? await BusbudService.shared.fetchSuggestions(for: coordinate) - - DispatchQueue.main.async { - self.suggestions = suggestions ?? [] - } - } - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return suggestions.count + return viewModel.suggestions.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) - let suggestion = suggestions[indexPath.row] + let suggestion = viewModel.suggestions[indexPath.row] cell.textLabel?.text = suggestion.cityName cell.detailTextLabel?.text = suggestion.formattedDistance @@ -65,8 +67,10 @@ class SuggestionsListViewController: UITableViewController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let suggestion = suggestions[indexPath.row] - let suggestionDetailViewController = SuggestionDetailViewController(suggestion: suggestion) + let suggestion = viewModel.suggestions[indexPath.row] + let suggestionDetailViewModel = SuggestionDetailViewModel(suggestion: suggestion) + let suggestionDetailViewController = SuggestionDetailViewController(viewModel: suggestionDetailViewModel) + navigationController?.pushViewController(suggestionDetailViewController, animated: true) } } diff --git a/busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift b/busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift new file mode 100644 index 0000000..79686e3 --- /dev/null +++ b/busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift @@ -0,0 +1,41 @@ +// +// SuggestionDetailViewModel.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import Foundation +import CoreLocation + +class SuggestionDetailViewModel { + private let suggestion: Suggestion + + init(suggestion: Suggestion) { + self.suggestion = suggestion + } + + var city: String { + return suggestion.cityName + } + + var region: String { + return suggestion.regionName + } + + var country: String { + return suggestion.countryName + } + + var distance: String { + suggestion.formattedDistance + } + + var geohash: String { + return suggestion.geohash + } + + var coordinate: CLLocationCoordinate2D { + CLLocationCoordinate2D(latitude: suggestion.lat, longitude: suggestion.lon) + } +} diff --git a/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift b/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift new file mode 100644 index 0000000..816bd49 --- /dev/null +++ b/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift @@ -0,0 +1,19 @@ +// +// SuggestionsListViewModel.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import Foundation +import CoreLocation + +@MainActor +class SuggestionsListViewModel { + var suggestions: [Suggestion] = [] + + func loadData(for coordinate: CLLocationCoordinate2D) async { + let fetchedSuggestions = try? await BusbudService.shared.fetchSuggestions(for: coordinate) + suggestions = fetchedSuggestions ?? [] + } +} From 229cf48e85a8d700e76b706c0164b419809adf6c Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 21:22:14 -0300 Subject: [PATCH 09/13] - Created SwiftUI version - Adjustments to UI --- .../ViewControllers/HomeViewController.swift | 14 ++- .../SuggestionDetailViewController.swift | 1 - .../SuggestionsListViewController.swift | 7 +- .../SuggestionDetailViewModel.swift | 2 +- .../ViewModels/SuggestionsListViewModel.swift | 4 +- .../Views/SuggestionDetailView.swift | 110 ++++++++++++++++++ .../Views/SuggestionsListView.swift | 81 +++++++++++++ 7 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 busbud-coding-challenge/Views/SuggestionDetailView.swift create mode 100644 busbud-coding-challenge/Views/SuggestionsListView.swift diff --git a/busbud-coding-challenge/ViewControllers/HomeViewController.swift b/busbud-coding-challenge/ViewControllers/HomeViewController.swift index 4eb5717..88e983d 100644 --- a/busbud-coding-challenge/ViewControllers/HomeViewController.swift +++ b/busbud-coding-challenge/ViewControllers/HomeViewController.swift @@ -6,6 +6,7 @@ // import UIKit +import SwiftUI class HomeViewController: UIViewController { private var stackViewMain: UIStackView = { @@ -68,7 +69,6 @@ class HomeViewController: UIViewController { super.viewDidLoad() title = "Home" - navigationItem.backButtonTitle = "" setupUI() } @@ -99,11 +99,19 @@ class HomeViewController: UIViewController { @objc private func startUIKitDemo() { let suggestionsViewModel = SuggestionsListViewModel() let suggestionsViewController = SuggestionsListViewController(viewModel: suggestionsViewModel) + let navigationViewController = UINavigationController(rootViewController: suggestionsViewController) + navigationViewController.modalPresentationStyle = .fullScreen - navigationController?.pushViewController(suggestionsViewController, animated: true) + present(navigationViewController, animated: true) } @objc private func startSwiftUIDemo() { - print("SwiftUI Demo") + let suggestionsViewModel = SuggestionsListViewModel() + let suggestionsView = SuggestionsListView(viewModel: suggestionsViewModel) + + let hostingController = UIHostingController(rootView: suggestionsView) + hostingController.modalPresentationStyle = .fullScreen + + present(hostingController, animated: true) } } diff --git a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift index b181983..3fb8bdf 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift @@ -146,7 +146,6 @@ class SuggestionDetailViewController: UIViewController { super.viewDidLoad() title = "Details" - navigationItem.backButtonTitle = "" setupUI() loadInfo() diff --git a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift index 9cb4ae6..f8c4bd7 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionsListViewController.swift @@ -24,8 +24,9 @@ class SuggestionsListViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Suggestions" + title = "Suggestions (UIKit)" navigationItem.backButtonTitle = "" + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Dismiss", style: .plain, target: self, action: #selector(dismissViewController)) setupUI() @@ -49,6 +50,10 @@ class SuggestionsListViewController: UITableViewController { tableView.separatorInset = .zero } + @objc private func dismissViewController() { + dismiss(animated: true) + } + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewModel.suggestions.count } diff --git a/busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift b/busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift index 79686e3..9bb953b 100644 --- a/busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift +++ b/busbud-coding-challenge/ViewModels/SuggestionDetailViewModel.swift @@ -8,7 +8,7 @@ import Foundation import CoreLocation -class SuggestionDetailViewModel { +class SuggestionDetailViewModel: ObservableObject { private let suggestion: Suggestion init(suggestion: Suggestion) { diff --git a/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift b/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift index 816bd49..89a9580 100644 --- a/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift +++ b/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift @@ -9,8 +9,8 @@ import Foundation import CoreLocation @MainActor -class SuggestionsListViewModel { - var suggestions: [Suggestion] = [] +class SuggestionsListViewModel: ObservableObject { + @Published var suggestions: [Suggestion] = [] func loadData(for coordinate: CLLocationCoordinate2D) async { let fetchedSuggestions = try? await BusbudService.shared.fetchSuggestions(for: coordinate) diff --git a/busbud-coding-challenge/Views/SuggestionDetailView.swift b/busbud-coding-challenge/Views/SuggestionDetailView.swift new file mode 100644 index 0000000..5298918 --- /dev/null +++ b/busbud-coding-challenge/Views/SuggestionDetailView.swift @@ -0,0 +1,110 @@ +// +// SuggestionDetailView.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import SwiftUI +import MapKit + +struct SuggestionDetailView: View { + @ObservedObject var viewModel: SuggestionDetailViewModel + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 14) { + LabelView(title: "City", value: viewModel.city) + LabelView(title: "State / Province / Region", value: viewModel.region) + LabelView(title: "Country", value: viewModel.country) + LabelView(title: "Distance", value: viewModel.distance) + + MapView(coordinate: viewModel.coordinate, city: viewModel.city) + .frame(height: 300) + .cornerRadius(8) + .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray.opacity(0.5), lineWidth: 1)) + .padding(.vertical, 8) + + Button(action: goToWebsite) { + Text("Go to Busbud Website") + .foregroundColor(.primary) + .frame(maxWidth: .infinity) + .padding() + .background(.orange) + .cornerRadius(8) + } + } + .padding(.horizontal, 20) + .navigationTitle("Details") + .navigationBarTitleDisplayMode(.inline) + } + .background(Color(UIColor.systemBackground)) + } + + private func goToWebsite() { + if let url = URL(string: "https://www.busbud.com/en/c/\(viewModel.geohash)") { + UIApplication.shared.open(url) + } + } +} + +struct LabelView: View { + let title: String + let value: String + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 16)) + .foregroundColor(Color.secondary) + Text(value) + .font(.system(size: 16, weight: .bold)) + .foregroundColor(Color.primary) + } + } +} + +struct MapView: View { + let coordinate: CLLocationCoordinate2D + let city: String + + @State private var region: MKCoordinateRegion + + init(coordinate: CLLocationCoordinate2D, city: String) { + self.coordinate = coordinate + self.city = city + _region = State(initialValue: MKCoordinateRegion( + center: coordinate, + latitudinalMeters: 10000, + longitudinalMeters: 10000 + )) + } + + var body: some View { + Map(position: .constant(.region(region)), interactionModes: []) { + Annotation(city, coordinate: coordinate) { + Image(systemName: "mappin.circle.fill") + .foregroundColor(.red) + .font(.title) + } + } + .scrollDisabled(true) + .onAppear { + updateRegion() + } + } + + private func updateRegion() { + region = MKCoordinateRegion( + center: coordinate, + latitudinalMeters: 10000, + longitudinalMeters: 10000 + ) + } +} + +struct MapAnnotationItem: Identifiable { + let id = UUID() + let coordinate: CLLocationCoordinate2D + let title: String +} diff --git a/busbud-coding-challenge/Views/SuggestionsListView.swift b/busbud-coding-challenge/Views/SuggestionsListView.swift new file mode 100644 index 0000000..de9a378 --- /dev/null +++ b/busbud-coding-challenge/Views/SuggestionsListView.swift @@ -0,0 +1,81 @@ +// +// SuggestionsListView.swift +// busbud-coding-challenge +// +// Created by Spencer Diniz on 17/12/24. +// + +import SwiftUI +import CoreLocation + +struct SuggestionsListView: View { + @Environment(\.dismiss) private var dismiss + @ObservedObject private var viewModel: SuggestionsListViewModel + @State private var isLoading = false + @State private var locationError: String? + + init(viewModel: SuggestionsListViewModel) { + self.viewModel = viewModel + } + + var body: some View { + NavigationView { + Group { + if isLoading { + ProgressView("Loading suggestions...") + } else if let error = locationError { + Text("Error: \(error)").foregroundColor(.red) + } else { + List(viewModel.suggestions, id: \.id) { suggestion in + NavigationLink(destination: SuggestionDetailView(viewModel: SuggestionDetailViewModel(suggestion: suggestion))) { + SuggestionRowView(suggestion: suggestion) + } + } + } + } + .navigationTitle("Suggestions (SwiftUI)") + .navigationBarTitleDisplayMode(.inline) + .onAppear { + fetchLocationAndLoadData() + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + dismiss() + }) { + Text("Dismiss") + } + } + } + } + } + + private func fetchLocationAndLoadData() { + Task { + isLoading = true + do { + let locationService = LocationService() + let coordinate = try await locationService.requestLocation() + await viewModel.loadData(for: coordinate) + } catch { + locationError = error.localizedDescription + } + isLoading = false + } + } +} + +// Row View +struct SuggestionRowView: View { + let suggestion: Suggestion + + var body: some View { + VStack(alignment: .leading) { + Text(suggestion.cityName) + .font(.headline) + Text(suggestion.formattedDistance) + .font(.subheadline) + .foregroundColor(.secondary) + } + } +} From 2a162032423350616c6d360ad3a45a0f8770100a Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Tue, 17 Dec 2024 21:59:48 -0300 Subject: [PATCH 10/13] - Added localization for English (US). - Added localization for Portuguese (BR). --- .../project.pbxproj | 3 + .../busbud-coding-challenge.xcscheme | 1 + busbud-coding-challenge/Localizable.xcstrings | 143 ++++++++++++++++++ .../Service/BusbudService.swift | 10 +- .../ViewControllers/HomeViewController.swift | 6 +- .../SuggestionDetailViewController.swift | 12 +- .../Views/SuggestionDetailView.swift | 2 +- 7 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 busbud-coding-challenge/Localizable.xcstrings diff --git a/busbud-coding-challenge.xcodeproj/project.pbxproj b/busbud-coding-challenge.xcodeproj/project.pbxproj index c9fa5c3..f987769 100644 --- a/busbud-coding-challenge.xcodeproj/project.pbxproj +++ b/busbud-coding-challenge.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ knownRegions = ( en, Base, + "pt-BR", ); mainGroup = 14BBD4432D11E6240047FE7A; minimizedProjectReferenceProxies = 1; @@ -405,6 +406,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -461,6 +463,7 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme index dce3eba..75b5eda 100644 --- a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme +++ b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme @@ -58,6 +58,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "en" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/busbud-coding-challenge/Localizable.xcstrings b/busbud-coding-challenge/Localizable.xcstrings new file mode 100644 index 0000000..3fe4419 --- /dev/null +++ b/busbud-coding-challenge/Localizable.xcstrings @@ -0,0 +1,143 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Choose implmentation:" : { + "extractionState" : "manual", + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escolha a implementação:" + } + } + } + }, + "City" : { + "extractionState" : "manual", + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cidade" + } + } + } + }, + "Country" : { + "extractionState" : "manual", + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "País" + } + } + } + }, + "Details" : { + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detalhes" + } + } + } + }, + "Dismiss" : { + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sair" + } + } + } + }, + "Distance" : { + "extractionState" : "manual", + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Distância" + } + } + } + }, + "Error: %@" : { + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro: %@" + } + } + } + }, + "Go to Busbud Website" : { + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vá para Website Busbud" + } + } + } + }, + "Loading suggestions..." : { + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carregando sugestões..." + } + } + } + }, + "State / Province / Region" : { + "extractionState" : "manual", + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado / Província / Região" + } + } + } + }, + "Suggestions (SwiftUI)" : { + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sugestões (SwiftUI)" + } + } + } + }, + "SwiftUI" : { + "extractionState" : "manual", + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "SwiftUI-BR" + } + } + } + }, + "UIKit" : { + "extractionState" : "manual", + "localizations" : { + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "UIKit-BR" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/busbud-coding-challenge/Service/BusbudService.swift b/busbud-coding-challenge/Service/BusbudService.swift index e32d30f..89b5f17 100644 --- a/busbud-coding-challenge/Service/BusbudService.swift +++ b/busbud-coding-challenge/Service/BusbudService.swift @@ -11,8 +11,16 @@ import CoreLocation public class BusbudService { public static let shared = BusbudService() + private let supportedLanguages: [String: String] = [ + "en": "en", + "pt": "pt" + ] + func fetchSuggestions(for coordinate: CLLocationCoordinate2D) async throws -> [Suggestion] { - let url = URL(string: "https://napi.busbud.com/flex/suggestions/points-of-interest?lang=en&limit=100&lat=\(coordinate.latitude)&lon=\(coordinate.longitude)")! + let deviceLanguage = Locale.current.language.languageCode?.identifier ?? "en" + let currantLanguage = supportedLanguages[deviceLanguage] ?? "en" + + let url = URL(string: "https://napi.busbud.com/flex/suggestions/points-of-interest?lang=\(currantLanguage)&limit=100&lat=\(coordinate.latitude)&lon=\(coordinate.longitude)")! var request = URLRequest(url: url) request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData request.httpMethod = "GET" diff --git a/busbud-coding-challenge/ViewControllers/HomeViewController.swift b/busbud-coding-challenge/ViewControllers/HomeViewController.swift index 88e983d..c3a654c 100644 --- a/busbud-coding-challenge/ViewControllers/HomeViewController.swift +++ b/busbud-coding-challenge/ViewControllers/HomeViewController.swift @@ -32,7 +32,7 @@ class HomeViewController: UIViewController { private var labelTitle: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = "Choose implmentation:" + label.text = String(localized: "Choose implmentation:") label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 20) @@ -42,7 +42,7 @@ class HomeViewController: UIViewController { private var buttonUIKit: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle("UIKit", for: .normal) + button.setTitle(String(localized: "UIKit"), for: .normal) button.setTitleColor(.darkText, for: .normal) button.backgroundColor = .systemOrange button.clipsToBounds = true @@ -55,7 +55,7 @@ class HomeViewController: UIViewController { private var buttonSwiftUI: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle("SwiftUI", for: .normal) + button.setTitle(String(localized: "SwiftUI"), for: .normal) button.setTitleColor(.darkText, for: .normal) button.backgroundColor = .systemOrange button.clipsToBounds = true diff --git a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift index 3fb8bdf..bb46ba3 100644 --- a/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift +++ b/busbud-coding-challenge/ViewControllers/SuggestionDetailViewController.swift @@ -31,7 +31,7 @@ class SuggestionDetailViewController: UIViewController { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 16) - label.text = "City" + label.text = String(localized: "City") label.textColor = .secondaryLabel return label @@ -50,7 +50,7 @@ class SuggestionDetailViewController: UIViewController { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 16) - label.text = "State / Province / Region" + label.text = String(localized: "State / Province / Region") label.textColor = .secondaryLabel return label @@ -69,7 +69,7 @@ class SuggestionDetailViewController: UIViewController { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 16) - label.text = "Country" + label.text = String(localized: "Country") label.textColor = .secondaryLabel return label @@ -88,7 +88,7 @@ class SuggestionDetailViewController: UIViewController { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 16) - label.text = "Distance" + label.text = String(localized: "Distance") label.textColor = .secondaryLabel return label @@ -120,7 +120,7 @@ class SuggestionDetailViewController: UIViewController { let buttonGoToWebsite: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle("Go to Busbud Website", for: .normal) + button.setTitle(String(localized: "Go to Busbud Website"), for: .normal) button.setTitleColor(.darkText, for: .normal) button.backgroundColor = .systemOrange button.clipsToBounds = true @@ -145,7 +145,7 @@ class SuggestionDetailViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Details" + title = String(localized: "Details") setupUI() loadInfo() diff --git a/busbud-coding-challenge/Views/SuggestionDetailView.swift b/busbud-coding-challenge/Views/SuggestionDetailView.swift index 5298918..b6e0221 100644 --- a/busbud-coding-challenge/Views/SuggestionDetailView.swift +++ b/busbud-coding-challenge/Views/SuggestionDetailView.swift @@ -49,7 +49,7 @@ struct SuggestionDetailView: View { } struct LabelView: View { - let title: String + let title: LocalizedStringKey let value: String var body: some View { From 4d20ea9f1b5e5baf945ab791106f2f502ee2ec3c Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Wed, 18 Dec 2024 00:37:44 -0300 Subject: [PATCH 11/13] - Added unit tests for service models. - Added unit tests for service. - Added unit tests for view models. - Added unit tests for view controllers. --- .../project.pbxproj | 112 +----------- .../busbud-coding-challenge.xcscheme | 9 +- busbud-coding-challenge.xctestplan | 29 ++++ .../Service/BusbudService.swift | 6 +- .../ViewModels/SuggestionsListViewModel.swift | 7 +- .../ServiceModelsUnitTests.swift | 164 ++++++++++++++++++ .../SuggestionDetailViewControllerTests.swift | 78 +++++++++ .../SuggestionDetailViewModelTests.swift | 87 ++++++++++ .../SuggestionsListViewModelTests.swift | 103 +++++++++++ .../busbud_coding_challengeTests.swift | 17 -- .../busbud_coding_challengeUITests.swift | 43 ----- ...d_coding_challengeUITestsLaunchTests.swift | 33 ---- 12 files changed, 481 insertions(+), 207 deletions(-) create mode 100644 busbud-coding-challenge.xctestplan create mode 100644 busbud-coding-challengeTests/ServiceModelsUnitTests.swift create mode 100644 busbud-coding-challengeTests/SuggestionDetailViewControllerTests.swift create mode 100644 busbud-coding-challengeTests/SuggestionDetailViewModelTests.swift create mode 100644 busbud-coding-challengeTests/SuggestionsListViewModelTests.swift delete mode 100644 busbud-coding-challengeTests/busbud_coding_challengeTests.swift delete mode 100644 busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift delete mode 100644 busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift diff --git a/busbud-coding-challenge.xcodeproj/project.pbxproj b/busbud-coding-challenge.xcodeproj/project.pbxproj index f987769..20053ff 100644 --- a/busbud-coding-challenge.xcodeproj/project.pbxproj +++ b/busbud-coding-challenge.xcodeproj/project.pbxproj @@ -14,19 +14,12 @@ remoteGlobalIDString = 14BBD44B2D11E6240047FE7A; remoteInfo = "busbud-coding-challenge"; }; - 14BBD46D2D11E6250047FE7A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 14BBD4442D11E6240047FE7A /* Project object */; - proxyType = 1; - remoteGlobalIDString = 14BBD44B2D11E6240047FE7A; - remoteInfo = "busbud-coding-challenge"; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 14BBD44C2D11E6240047FE7A /* busbud-coding-challenge.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "busbud-coding-challenge.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 14BBD4622D11E6250047FE7A /* busbud-coding-challengeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "busbud-coding-challengeTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 14BBD46C2D11E6250047FE7A /* busbud-coding-challengeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "busbud-coding-challengeUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14BBD4F72D12796F0047FE7A /* busbud-coding-challenge.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "busbud-coding-challenge.xctestplan"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -53,11 +46,6 @@ path = "busbud-coding-challengeTests"; sourceTree = ""; }; - 14BBD46F2D11E6250047FE7A /* busbud-coding-challengeUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = "busbud-coding-challengeUITests"; - sourceTree = ""; - }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,22 +63,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 14BBD4692D11E6250047FE7A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 14BBD4432D11E6240047FE7A = { isa = PBXGroup; children = ( + 14BBD4F72D12796F0047FE7A /* busbud-coding-challenge.xctestplan */, 14BBD44E2D11E6240047FE7A /* busbud-coding-challenge */, 14BBD4652D11E6250047FE7A /* busbud-coding-challengeTests */, - 14BBD46F2D11E6250047FE7A /* busbud-coding-challengeUITests */, 14BBD44D2D11E6240047FE7A /* Products */, ); sourceTree = ""; @@ -100,7 +81,6 @@ children = ( 14BBD44C2D11E6240047FE7A /* busbud-coding-challenge.app */, 14BBD4622D11E6250047FE7A /* busbud-coding-challengeTests.xctest */, - 14BBD46C2D11E6250047FE7A /* busbud-coding-challengeUITests.xctest */, ); name = Products; sourceTree = ""; @@ -153,29 +133,6 @@ productReference = 14BBD4622D11E6250047FE7A /* busbud-coding-challengeTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 14BBD46B2D11E6250047FE7A /* busbud-coding-challengeUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 14BBD47D2D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challengeUITests" */; - buildPhases = ( - 14BBD4682D11E6250047FE7A /* Sources */, - 14BBD4692D11E6250047FE7A /* Frameworks */, - 14BBD46A2D11E6250047FE7A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 14BBD46E2D11E6250047FE7A /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 14BBD46F2D11E6250047FE7A /* busbud-coding-challengeUITests */, - ); - name = "busbud-coding-challengeUITests"; - packageProductDependencies = ( - ); - productName = "busbud-coding-challengeUITests"; - productReference = 14BBD46C2D11E6250047FE7A /* busbud-coding-challengeUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -193,10 +150,6 @@ CreatedOnToolsVersion = 16.2; TestTargetID = 14BBD44B2D11E6240047FE7A; }; - 14BBD46B2D11E6250047FE7A = { - CreatedOnToolsVersion = 16.2; - TestTargetID = 14BBD44B2D11E6240047FE7A; - }; }; }; buildConfigurationList = 14BBD4472D11E6240047FE7A /* Build configuration list for PBXProject "busbud-coding-challenge" */; @@ -216,7 +169,6 @@ targets = ( 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */, 14BBD4612D11E6250047FE7A /* busbud-coding-challengeTests */, - 14BBD46B2D11E6250047FE7A /* busbud-coding-challengeUITests */, ); }; /* End PBXProject section */ @@ -236,13 +188,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 14BBD46A2D11E6250047FE7A /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -260,13 +205,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 14BBD4682D11E6250047FE7A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -275,11 +213,6 @@ target = 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */; targetProxy = 14BBD4632D11E6250047FE7A /* PBXContainerItemProxy */; }; - 14BBD46E2D11E6250047FE7A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 14BBD44B2D11E6240047FE7A /* busbud-coding-challenge */; - targetProxy = 14BBD46D2D11E6250047FE7A /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -504,38 +437,6 @@ }; name = Release; }; - 14BBD47E2D11E6250047FE7A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challengeUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "busbud-coding-challenge"; - }; - name = Debug; - }; - 14BBD47F2D11E6250047FE7A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "spencer.busbud-coding-challengeUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "busbud-coding-challenge"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -566,15 +467,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 14BBD47D2D11E6250047FE7A /* Build configuration list for PBXNativeTarget "busbud-coding-challengeUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 14BBD47E2D11E6250047FE7A /* Debug */, - 14BBD47F2D11E6250047FE7A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 14BBD4442D11E6240047FE7A /* Project object */; diff --git a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme index 75b5eda..6f4cc20 100644 --- a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme +++ b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme @@ -27,8 +27,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + [Suggestion] +} + +public class BusbudService: BusbudServiceProtocol { public static let shared = BusbudService() private let supportedLanguages: [String: String] = [ diff --git a/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift b/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift index 89a9580..8b27ace 100644 --- a/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift +++ b/busbud-coding-challenge/ViewModels/SuggestionsListViewModel.swift @@ -11,9 +11,14 @@ import CoreLocation @MainActor class SuggestionsListViewModel: ObservableObject { @Published var suggestions: [Suggestion] = [] + private let service: BusbudServiceProtocol + + init(service: BusbudServiceProtocol = BusbudService.shared) { + self.service = service + } func loadData(for coordinate: CLLocationCoordinate2D) async { - let fetchedSuggestions = try? await BusbudService.shared.fetchSuggestions(for: coordinate) + let fetchedSuggestions = try? await service.fetchSuggestions(for: coordinate) suggestions = fetchedSuggestions ?? [] } } diff --git a/busbud-coding-challengeTests/ServiceModelsUnitTests.swift b/busbud-coding-challengeTests/ServiceModelsUnitTests.swift new file mode 100644 index 0000000..f0a9370 --- /dev/null +++ b/busbud-coding-challengeTests/ServiceModelsUnitTests.swift @@ -0,0 +1,164 @@ +// +// ServiceModelsUnitTests.swift +// busbud-coding-challengeTests +// +// Created by Spencer Diniz on 17/12/24. +// + +import XCTest +@testable import busbud_coding_challenge + +final class ServiceModelsTests: XCTestCase { + func testDecodingSuggestionsResponse() throws { + let json = """ + { + "suggestions": [ + { + "provider": "testProvider", + "id": "123", + "place_id": "place123", + "geo_entity_id": "geo123", + "parent_id": "parent123", + "label": "Test Label", + "score": 0.9, + "place_type": "city", + "stop_type": "bus_stop", + "url": "https://example.com", + "geohash": "u4pruydqqvj", + "lat": 40.7128, + "lon": -74.0060, + "hierarchy_info": { + "country": { + "code": "US", + "name": "United States" + }, + "region": { + "code": "NY", + "name": "New York" + } + }, + "city_name": "New York", + "region_name": "New York", + "country_name": "United States", + "full_name": "New York, United States", + "short_name": "NY", + "request_id": "req123" + } + ], + "results_type": "test" + } + """ + + let jsonData = Data(json.utf8) + let decoder = JSONDecoder() + + let response = try decoder.decode(SuggestionsResponse.self, from: jsonData) + + XCTAssertEqual(response.resultsType, "test") + XCTAssertEqual(response.suggestions.count, 1) + + let suggestion = response.suggestions.first! + XCTAssertEqual(suggestion.provider, "testProvider") + XCTAssertEqual(suggestion.id, "123") + XCTAssertEqual(suggestion.label, "Test Label") + XCTAssertEqual(suggestion.hierarchyInfo.country.name, "United States") + XCTAssertEqual(suggestion.hierarchyInfo.region.name, "New York") + } + + func testFormattedDistanceInKm() { + var suggestion = Suggestion( + provider: "testProvider", + id: "123", + placeID: "place123", + geoEntityID: "geo123", + parentID: nil, + label: "Test Label", + score: 0.9, + placeType: "city", + stopType: "bus_stop", + url: "https://example.com", + geohash: "u4pruydqqvj", + lat: 40.7128, + lon: -74.0060, + hierarchyInfo: HierarchyInfo( + country: Country(code: "US", name: "United States"), + region: Region(code: "NY", name: "New York") + ), + cityName: "New York", + regionName: "New York", + countryName: "United States", + fullName: "New York, United States", + shortName: "NY", + requestID: "req123", + distance: nil + ) + + XCTAssertEqual(suggestion.formattedDistanceInKm, "") + suggestion.distance = 1500 + XCTAssertEqual(suggestion.formattedDistanceInKm, "1.50 km") + } + + func testFormattedDistanceInMiles() { + var suggestion = Suggestion( + provider: "testProvider", + id: "123", + placeID: "place123", + geoEntityID: "geo123", + parentID: nil, + label: "Test Label", + score: 0.9, + placeType: "city", + stopType: "bus_stop", + url: "https://example.com", + geohash: "u4pruydqqvj", + lat: 40.7128, + lon: -74.0060, + hierarchyInfo: HierarchyInfo( + country: Country(code: "US", name: "United States"), + region: Region(code: "NY", name: "New York") + ), + cityName: "New York", + regionName: "New York", + countryName: "United States", + fullName: "New York, United States", + shortName: "NY", + requestID: "req123", + distance: nil + ) + + XCTAssertEqual(suggestion.formattedDistanceInMiles, "") + suggestion.distance = 1609.34 + XCTAssertEqual(suggestion.formattedDistanceInMiles, "1.00 mi") + } + + func testFormattedDistance() { + let suggestion = Suggestion( + provider: "testProvider", + id: "123", + placeID: "place123", + geoEntityID: "geo123", + parentID: nil, + label: "Test Label", + score: 0.9, + placeType: "city", + stopType: "bus_stop", + url: "https://example.com", + geohash: "u4pruydqqvj", + lat: 40.7128, + lon: -74.0060, + hierarchyInfo: HierarchyInfo( + country: Country(code: "US", name: "United States"), + region: Region(code: "NY", name: "New York") + ), + cityName: "New York", + regionName: "New York", + countryName: "United States", + fullName: "New York, United States", + shortName: "NY", + requestID: "req123", + distance: 3000 + ) + + XCTAssertEqual(suggestion.formattedDistance, "3.00 km / 1.86 mi") + } +} diff --git a/busbud-coding-challengeTests/SuggestionDetailViewControllerTests.swift b/busbud-coding-challengeTests/SuggestionDetailViewControllerTests.swift new file mode 100644 index 0000000..e95f447 --- /dev/null +++ b/busbud-coding-challengeTests/SuggestionDetailViewControllerTests.swift @@ -0,0 +1,78 @@ +// +// SuggestionDetailViewControllerTests.swift +// busbud-coding-challengeTests +// +// Created by Spencer Diniz on 18/12/24. +// + +import XCTest +import MapKit +@testable import busbud_coding_challenge + +final class SuggestionDetailViewControllerTests: XCTestCase { + private func createMockViewModel() -> SuggestionDetailViewModel { + let mockSuggestion = Suggestion( + provider: "mockProvider", + id: "1", + placeID: "place1", + geoEntityID: "geo1", + parentID: nil, + label: "Test Label", + score: 0.9, + placeType: "city", + stopType: "stop", + url: "https://example.com", + geohash: "abc123", + lat: 40.7128, + lon: -74.0060, + hierarchyInfo: HierarchyInfo( + country: Country(code: "US", name: "United States"), + region: Region(code: "NY", name: "New York") + ), + cityName: "New York", + regionName: "New York", + countryName: "United States", + fullName: "New York, United States", + shortName: "NY", + requestID: "req1", + distance: 1500 + ) + return SuggestionDetailViewModel(suggestion: mockSuggestion) + } + + func testViewControllerLoadsInfoCorrectly() { + let viewModel = createMockViewModel() + let sut = SuggestionDetailViewController(viewModel: viewModel) // System Under Test (SUT) + + sut.loadViewIfNeeded() + + XCTAssertEqual(sut.labelCityValue.text, "New York") + XCTAssertEqual(sut.labelRegionValue.text, "New York") + XCTAssertEqual(sut.labelCountryValue.text, "United States") + XCTAssertEqual(sut.labelDistanceValue.text, "1.50 km / 0.93 mi") + } + + func testMapViewDisplaysCorrectRegionAndAnnotation() { + let viewModel = createMockViewModel() + let sut = SuggestionDetailViewController(viewModel: viewModel) + + sut.loadViewIfNeeded() + sut.view.layoutIfNeeded() + + let mapViewRegion = sut.mapView.region + let expectedRegion = MKCoordinateRegion( + center: CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060), + latitudinalMeters: 10000, + longitudinalMeters: 10000 + ) + + XCTAssertEqual(mapViewRegion.center.latitude, expectedRegion.center.latitude, accuracy: 0.01) + XCTAssertEqual(mapViewRegion.center.longitude, expectedRegion.center.longitude, accuracy: 0.01) + XCTAssertEqual(sut.mapView.annotations.count, 1) + + let annotation = sut.mapView.annotations.first as? MKPointAnnotation + XCTAssertEqual(annotation!.coordinate.latitude, 40.7128, accuracy: 0.01) + XCTAssertEqual(annotation!.coordinate.longitude, -74.0060, accuracy: 0.01) + XCTAssertEqual(annotation?.title, "New York") + } +} diff --git a/busbud-coding-challengeTests/SuggestionDetailViewModelTests.swift b/busbud-coding-challengeTests/SuggestionDetailViewModelTests.swift new file mode 100644 index 0000000..394326b --- /dev/null +++ b/busbud-coding-challengeTests/SuggestionDetailViewModelTests.swift @@ -0,0 +1,87 @@ +// +// SuggestionDetailViewModelTests.swift +// busbud-coding-challengeTests +// +// Created by Spencer Diniz on 18/12/24. +// + +import XCTest +import CoreLocation +@testable import busbud_coding_challenge + +final class SuggestionDetailViewModelTests: XCTestCase { + func testCityProperty() { + let suggestion = createMockSuggestion() + let viewModel = SuggestionDetailViewModel(suggestion: suggestion) + + XCTAssertEqual(viewModel.city, "New York") + } + + func testRegionProperty() { + let suggestion = createMockSuggestion() + let viewModel = SuggestionDetailViewModel(suggestion: suggestion) + + XCTAssertEqual(viewModel.region, "New York") + } + + func testCountryProperty() { + let suggestion = createMockSuggestion() + let viewModel = SuggestionDetailViewModel(suggestion: suggestion) + + XCTAssertEqual(viewModel.country, "United States") + } + + func testDistanceProperty() { + var suggestion = createMockSuggestion() + suggestion.distance = 1500 // Distance in meters + let viewModel = SuggestionDetailViewModel(suggestion: suggestion) + + XCTAssertEqual(viewModel.distance, "1.50 km / 0.93 mi") + } + + func testGeohashProperty() { + let suggestion = createMockSuggestion() + let viewModel = SuggestionDetailViewModel(suggestion: suggestion) + + XCTAssertEqual(viewModel.geohash, "u4pruydqqvj") + } + + func testCoordinateProperty() { + let suggestion = createMockSuggestion() + let viewModel = SuggestionDetailViewModel(suggestion: suggestion) + let coordinate = viewModel.coordinate + + XCTAssertEqual(coordinate.latitude, 40.7128) + XCTAssertEqual(coordinate.longitude, -74.0060) + } + + // Helper function to create a mock Suggestion object + private func createMockSuggestion() -> Suggestion { + return Suggestion( + provider: "testProvider", + id: "123", + placeID: "place123", + geoEntityID: "geo123", + parentID: "parent123", + label: "Test Label", + score: 0.9, + placeType: "city", + stopType: "bus_stop", + url: "https://example.com", + geohash: "u4pruydqqvj", + lat: 40.7128, + lon: -74.0060, + hierarchyInfo: HierarchyInfo( + country: Country(code: "US", name: "United States"), + region: Region(code: "NY", name: "New York") + ), + cityName: "New York", + regionName: "New York", + countryName: "United States", + fullName: "New York, United States", + shortName: "NY", + requestID: "req123", + distance: nil + ) + } +} diff --git a/busbud-coding-challengeTests/SuggestionsListViewModelTests.swift b/busbud-coding-challengeTests/SuggestionsListViewModelTests.swift new file mode 100644 index 0000000..70ad789 --- /dev/null +++ b/busbud-coding-challengeTests/SuggestionsListViewModelTests.swift @@ -0,0 +1,103 @@ +// +// SuggestionsListViewModelTests.swift +// busbud-coding-challengeTests +// +// Created by Spencer Diniz on 18/12/24. +// + +import XCTest +import CoreLocation +@testable import busbud_coding_challenge + +@MainActor +final class SuggestionsListViewModelTests: XCTestCase { + func testLoadData_withMockedService_returnsSuggestions() async throws { + let mockService = MockBusbudService() + let viewModel = SuggestionsListViewModel(service: mockService) + let mockCoordinate = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060) + + await viewModel.loadData(for: mockCoordinate) + + XCTAssertEqual(viewModel.suggestions.count, 2) + XCTAssertEqual(viewModel.suggestions.first?.cityName, "New York") + XCTAssertEqual(viewModel.suggestions.last?.cityName, "San Francisco") + } + + func testLoadData_withMockedService_returnsEmptyWhenNoData() async throws { + let mockService = MockBusbudService(empty: true) + let viewModel = SuggestionsListViewModel(service: mockService) + let mockCoordinate = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060) + + await viewModel.loadData(for: mockCoordinate) + XCTAssertTrue(viewModel.suggestions.isEmpty) + } +} + +struct MockBusbudService: BusbudServiceProtocol { + let empty: Bool + + init(empty: Bool = false) { + self.empty = empty + } + + func fetchSuggestions(for coordinate: CLLocationCoordinate2D) async throws -> [Suggestion] { + if empty { + return [] + } else { + return [ + Suggestion( + provider: "mockProvider", + id: "1", + placeID: "place1", + geoEntityID: "geo1", + parentID: nil, + label: "Label 1", + score: 0.9, + placeType: "city", + stopType: "stop", + url: "https://example.com/1", + geohash: "abc123", + lat: 40.7128, + lon: -74.0060, + hierarchyInfo: HierarchyInfo( + country: Country(code: "US", name: "United States"), + region: Region(code: "NY", name: "New York") + ), + cityName: "New York", + regionName: "New York", + countryName: "United States", + fullName: "New York, United States", + shortName: "NY", + requestID: "req1", + distance: nil + ), + Suggestion( + provider: "mockProvider", + id: "2", + placeID: "place2", + geoEntityID: "geo2", + parentID: nil, + label: "Label 2", + score: 0.8, + placeType: "city", + stopType: "stop", + url: "https://example.com/2", + geohash: "xyz789", + lat: 37.7749, + lon: -122.4194, + hierarchyInfo: HierarchyInfo( + country: Country(code: "US", name: "United States"), + region: Region(code: "CA", name: "California") + ), + cityName: "San Francisco", + regionName: "California", + countryName: "United States", + fullName: "San Francisco, United States", + shortName: "SF", + requestID: "req2", + distance: nil + ) + ] + } + } +} diff --git a/busbud-coding-challengeTests/busbud_coding_challengeTests.swift b/busbud-coding-challengeTests/busbud_coding_challengeTests.swift deleted file mode 100644 index c38062f..0000000 --- a/busbud-coding-challengeTests/busbud_coding_challengeTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// busbud_coding_challengeTests.swift -// busbud-coding-challengeTests -// -// Created by Spencer Diniz on 17/12/24. -// - -import Testing -@testable import busbud_coding_challenge - -struct busbud_coding_challengeTests { - - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - } - -} diff --git a/busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift b/busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift deleted file mode 100644 index 89359e7..0000000 --- a/busbud-coding-challengeUITests/busbud_coding_challengeUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// busbud_coding_challengeUITests.swift -// busbud-coding-challengeUITests -// -// Created by Spencer Diniz on 17/12/24. -// - -import XCTest - -final class busbud_coding_challengeUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift b/busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift deleted file mode 100644 index 58d266a..0000000 --- a/busbud-coding-challengeUITests/busbud_coding_challengeUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// busbud_coding_challengeUITestsLaunchTests.swift -// busbud-coding-challengeUITests -// -// Created by Spencer Diniz on 17/12/24. -// - -import XCTest - -final class busbud_coding_challengeUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} From a0e5881b87f1dcb6bc875e45306f68f492d0993c Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Wed, 18 Dec 2024 00:46:36 -0300 Subject: [PATCH 12/13] Added localization for system messages. --- .../busbud-coding-challenge.xcscheme | 2 +- busbud-coding-challenge/InfoPlist.xcstrings | 59 +++++++++++++++++++ busbud-coding-challenge/Localizable.xcstrings | 4 +- 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 busbud-coding-challenge/InfoPlist.xcstrings diff --git a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme index 6f4cc20..d6705fe 100644 --- a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme +++ b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme @@ -63,7 +63,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "en" + language = "pt-BR" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/busbud-coding-challenge/InfoPlist.xcstrings b/busbud-coding-challenge/InfoPlist.xcstrings new file mode 100644 index 0000000..1f3c354 --- /dev/null +++ b/busbud-coding-challenge/InfoPlist.xcstrings @@ -0,0 +1,59 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Busbud" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Busbud BR" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "busbud-coding-challenge" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "busbud-coding-challenge" + } + } + } + }, + "NSLocationWhenInUseUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We use your location to provide better services." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usamos sua localização para fornecer melhores serviços." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/busbud-coding-challenge/Localizable.xcstrings b/busbud-coding-challenge/Localizable.xcstrings index 3fe4419..98ea709 100644 --- a/busbud-coding-challenge/Localizable.xcstrings +++ b/busbud-coding-challenge/Localizable.xcstrings @@ -122,7 +122,7 @@ "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "SwiftUI-BR" + "value" : "SwiftUI" } } } @@ -133,7 +133,7 @@ "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "UIKit-BR" + "value" : "UIKit" } } } From d9d3a0fa9a17b648043a2ff1cbaf022bf40db5d9 Mon Sep 17 00:00:00 2001 From: Spencer Diniz Date: Wed, 18 Dec 2024 01:35:04 -0300 Subject: [PATCH 13/13] Adjustments to localization. --- .../xcshareddata/xcschemes/busbud-coding-challenge.xcscheme | 2 +- busbud-coding-challenge/Localizable.xcstrings | 4 ++-- .../ViewControllers/HomeViewController.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme index d6705fe..6f4cc20 100644 --- a/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme +++ b/busbud-coding-challenge.xcodeproj/xcshareddata/xcschemes/busbud-coding-challenge.xcscheme @@ -63,7 +63,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "pt-BR" + language = "en" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/busbud-coding-challenge/Localizable.xcstrings b/busbud-coding-challenge/Localizable.xcstrings index 98ea709..57bbac8 100644 --- a/busbud-coding-challenge/Localizable.xcstrings +++ b/busbud-coding-challenge/Localizable.xcstrings @@ -1,13 +1,13 @@ { "sourceLanguage" : "en", "strings" : { - "Choose implmentation:" : { + "Choose the experience:" : { "extractionState" : "manual", "localizations" : { "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Escolha a implementação:" + "value" : "Escolha a experiência:" } } } diff --git a/busbud-coding-challenge/ViewControllers/HomeViewController.swift b/busbud-coding-challenge/ViewControllers/HomeViewController.swift index c3a654c..8b206f3 100644 --- a/busbud-coding-challenge/ViewControllers/HomeViewController.swift +++ b/busbud-coding-challenge/ViewControllers/HomeViewController.swift @@ -32,7 +32,7 @@ class HomeViewController: UIViewController { private var labelTitle: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = String(localized: "Choose implmentation:") + label.text = String(localized: "Choose the experience:") label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 20)