diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..15b2f42 Binary files /dev/null and b/.DS_Store differ diff --git a/Calculator/Calculator.xcodeproj/project.pbxproj b/Calculator/Calculator.xcodeproj/project.pbxproj new file mode 100644 index 0000000..309b483 --- /dev/null +++ b/Calculator/Calculator.xcodeproj/project.pbxproj @@ -0,0 +1,531 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + F652F5FC2EFED0210000E20A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F652F5F82EFED0210000E20A /* Assets.xcassets */; }; + F652F6002EFED0510000E20A /* CalculatorTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F5FF2EFED0510000E20A /* CalculatorTypes.swift */; }; + F652F6022EFED0850000E20A /* CalculatorEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F6012EFED0850000E20A /* CalculatorEngine.swift */; }; + F652F6042EFED0900000E20A /* DefaultCalculatorEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F6032EFED0900000E20A /* DefaultCalculatorEngine.swift */; }; + F652F6062EFED0C30000E20A /* CalculatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F6052EFED0C30000E20A /* CalculatorViewModel.swift */; }; + F652F6082EFED0F30000E20A /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F6072EFED0F30000E20A /* AppContainer.swift */; }; + F652F60A2EFED1030000E20A /* CalculatorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F6092EFED1030000E20A /* CalculatorApp.swift */; }; + F652F60C2EFED1160000E20A /* CalculatorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F60B2EFED1160000E20A /* CalculatorButton.swift */; }; + F652F60E2EFED1280000E20A /* CalculatorKeyStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F60D2EFED1280000E20A /* CalculatorKeyStyle.swift */; }; + F652F6102EFED14A0000E20A /* CalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F652F60F2EFED14A0000E20A /* CalculatorView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F652F61F2EFED2D60000E20A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F652F5D82EFECFBC0000E20A /* Project object */; + proxyType = 1; + remoteGlobalIDString = F652F5DF2EFECFBC0000E20A; + remoteInfo = Calculator; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + F652F5E02EFECFBC0000E20A /* Calculator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Calculator.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F652F5F82EFED0210000E20A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F652F5FF2EFED0510000E20A /* CalculatorTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorTypes.swift; sourceTree = ""; }; + F652F6012EFED0850000E20A /* CalculatorEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorEngine.swift; sourceTree = ""; }; + F652F6032EFED0900000E20A /* DefaultCalculatorEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultCalculatorEngine.swift; sourceTree = ""; }; + F652F6052EFED0C30000E20A /* CalculatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorViewModel.swift; sourceTree = ""; }; + F652F6072EFED0F30000E20A /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContainer.swift; sourceTree = ""; }; + F652F6092EFED1030000E20A /* CalculatorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorApp.swift; sourceTree = ""; }; + F652F60B2EFED1160000E20A /* CalculatorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorButton.swift; sourceTree = ""; }; + F652F60D2EFED1280000E20A /* CalculatorKeyStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorKeyStyle.swift; sourceTree = ""; }; + F652F60F2EFED14A0000E20A /* CalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorView.swift; sourceTree = ""; }; + F652F61B2EFED2D60000E20A /* CalculatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CalculatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + F652F61C2EFED2D60000E20A /* CalculatorTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CalculatorTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + F652F5DD2EFECFBC0000E20A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F652F6182EFED2D60000E20A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F652F5D72EFECFBC0000E20A = { + isa = PBXGroup; + children = ( + F652F5FB2EFED0210000E20A /* Calculator */, + F652F61C2EFED2D60000E20A /* CalculatorTests */, + F652F5E12EFECFBC0000E20A /* Products */, + ); + sourceTree = ""; + }; + F652F5E12EFECFBC0000E20A /* Products */ = { + isa = PBXGroup; + children = ( + F652F5E02EFECFBC0000E20A /* Calculator.app */, + F652F61B2EFED2D60000E20A /* CalculatorTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + F652F5F32EFED0210000E20A /* App */ = { + isa = PBXGroup; + children = ( + F652F6092EFED1030000E20A /* CalculatorApp.swift */, + F652F6072EFED0F30000E20A /* AppContainer.swift */, + ); + path = App; + sourceTree = ""; + }; + F652F5F42EFED0210000E20A /* Domain */ = { + isa = PBXGroup; + children = ( + F652F6032EFED0900000E20A /* DefaultCalculatorEngine.swift */, + F652F6012EFED0850000E20A /* CalculatorEngine.swift */, + F652F5FF2EFED0510000E20A /* CalculatorTypes.swift */, + ); + path = Domain; + sourceTree = ""; + }; + F652F5F52EFED0210000E20A /* Presentation */ = { + isa = PBXGroup; + children = ( + F652F6052EFED0C30000E20A /* CalculatorViewModel.swift */, + ); + path = Presentation; + sourceTree = ""; + }; + F652F5F72EFED0210000E20A /* UI */ = { + isa = PBXGroup; + children = ( + F652F60F2EFED14A0000E20A /* CalculatorView.swift */, + F652F60D2EFED1280000E20A /* CalculatorKeyStyle.swift */, + F652F60B2EFED1160000E20A /* CalculatorButton.swift */, + ); + path = UI; + sourceTree = ""; + }; + F652F5FB2EFED0210000E20A /* Calculator */ = { + isa = PBXGroup; + children = ( + F652F5F32EFED0210000E20A /* App */, + F652F5F42EFED0210000E20A /* Domain */, + F652F5F52EFED0210000E20A /* Presentation */, + F652F5F72EFED0210000E20A /* UI */, + F652F5F82EFED0210000E20A /* Assets.xcassets */, + ); + path = Calculator; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F652F5DF2EFECFBC0000E20A /* Calculator */ = { + isa = PBXNativeTarget; + buildConfigurationList = F652F5EB2EFECFBD0000E20A /* Build configuration list for PBXNativeTarget "Calculator" */; + buildPhases = ( + F652F5DC2EFECFBC0000E20A /* Sources */, + F652F5DD2EFECFBC0000E20A /* Frameworks */, + F652F5DE2EFECFBC0000E20A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Calculator; + packageProductDependencies = ( + ); + productName = Calculator; + productReference = F652F5E02EFECFBC0000E20A /* Calculator.app */; + productType = "com.apple.product-type.application"; + }; + F652F61A2EFED2D60000E20A /* CalculatorTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F652F6212EFED2D70000E20A /* Build configuration list for PBXNativeTarget "CalculatorTests" */; + buildPhases = ( + F652F6172EFED2D60000E20A /* Sources */, + F652F6182EFED2D60000E20A /* Frameworks */, + F652F6192EFED2D60000E20A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F652F6202EFED2D60000E20A /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + F652F61C2EFED2D60000E20A /* CalculatorTests */, + ); + name = CalculatorTests; + packageProductDependencies = ( + ); + productName = CalculatorTests; + productReference = F652F61B2EFED2D60000E20A /* CalculatorTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F652F5D82EFECFBC0000E20A /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2620; + LastUpgradeCheck = 2620; + TargetAttributes = { + F652F5DF2EFECFBC0000E20A = { + CreatedOnToolsVersion = 26.2; + }; + F652F61A2EFED2D60000E20A = { + CreatedOnToolsVersion = 26.2; + TestTargetID = F652F5DF2EFECFBC0000E20A; + }; + }; + }; + buildConfigurationList = F652F5DB2EFECFBC0000E20A /* Build configuration list for PBXProject "Calculator" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F652F5D72EFECFBC0000E20A; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = F652F5E12EFECFBC0000E20A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F652F5DF2EFECFBC0000E20A /* Calculator */, + F652F61A2EFED2D60000E20A /* CalculatorTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F652F5DE2EFECFBC0000E20A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F652F5FC2EFED0210000E20A /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F652F6192EFED2D60000E20A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F652F5DC2EFECFBC0000E20A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F652F6022EFED0850000E20A /* CalculatorEngine.swift in Sources */, + F652F60C2EFED1160000E20A /* CalculatorButton.swift in Sources */, + F652F6042EFED0900000E20A /* DefaultCalculatorEngine.swift in Sources */, + F652F6062EFED0C30000E20A /* CalculatorViewModel.swift in Sources */, + F652F6002EFED0510000E20A /* CalculatorTypes.swift in Sources */, + F652F6082EFED0F30000E20A /* AppContainer.swift in Sources */, + F652F60A2EFED1030000E20A /* CalculatorApp.swift in Sources */, + F652F6102EFED14A0000E20A /* CalculatorView.swift in Sources */, + F652F60E2EFED1280000E20A /* CalculatorKeyStyle.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F652F6172EFED2D60000E20A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F652F6202EFED2D60000E20A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F652F5DF2EFECFBC0000E20A /* Calculator */; + targetProxy = F652F61F2EFED2D60000E20A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + F652F5E92EFECFBD0000E20A /* 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 = 26.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; + }; + F652F5EA2EFECFBD0000E20A /* 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 = 26.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; + }; + F652F5EC2EFECFBD0000E20A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + 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 = com.koowihc.Calculator; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F652F5ED2EFECFBD0000E20A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + 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 = com.koowihc.Calculator; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + F652F6222EFED2D70000E20A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.koowihc.CalculatorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Calculator.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Calculator"; + }; + name = Debug; + }; + F652F6232EFED2D70000E20A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.koowihc.CalculatorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Calculator.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Calculator"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F652F5DB2EFECFBC0000E20A /* Build configuration list for PBXProject "Calculator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F652F5E92EFECFBD0000E20A /* Debug */, + F652F5EA2EFECFBD0000E20A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F652F5EB2EFECFBD0000E20A /* Build configuration list for PBXNativeTarget "Calculator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F652F5EC2EFECFBD0000E20A /* Debug */, + F652F5ED2EFECFBD0000E20A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F652F6212EFED2D70000E20A /* Build configuration list for PBXNativeTarget "CalculatorTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F652F6222EFED2D70000E20A /* Debug */, + F652F6232EFED2D70000E20A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F652F5D82EFECFBC0000E20A /* Project object */; +} diff --git a/Calculator/Calculator.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Calculator/Calculator.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Calculator/Calculator.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Calculator/Calculator/App/AppContainer.swift b/Calculator/Calculator/App/AppContainer.swift new file mode 100644 index 0000000..597e3fd --- /dev/null +++ b/Calculator/Calculator/App/AppContainer.swift @@ -0,0 +1,19 @@ +// +// AppContainer.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +struct AppContainer { + let engine: CalculatorEngine + + init(engine: CalculatorEngine = DefaultCalculatorEngine()) { + self.engine = engine + } + + func makeCalculatorViewModel() -> CalculatorViewModel { + CalculatorViewModel(engine: engine) + } +} \ No newline at end of file diff --git a/Calculator/Calculator/App/CalculatorApp.swift b/Calculator/Calculator/App/CalculatorApp.swift new file mode 100644 index 0000000..ef1b217 --- /dev/null +++ b/Calculator/Calculator/App/CalculatorApp.swift @@ -0,0 +1,20 @@ +// +// CalculatorApp.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import SwiftUI + +@main +struct CalculatorApp: App { + private let container = AppContainer() + + var body: some Scene { + WindowGroup { + CalculatorView(viewModel: container.makeCalculatorViewModel()) + } + } +} diff --git a/Calculator/Calculator/Assets.xcassets/AccentColor.colorset/Contents.json b/Calculator/Calculator/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Calculator/Calculator/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Calculator/Calculator/Assets.xcassets/AppIcon.appiconset/Contents.json b/Calculator/Calculator/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Calculator/Calculator/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/Calculator/Calculator/Assets.xcassets/Contents.json b/Calculator/Calculator/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Calculator/Calculator/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Calculator/Calculator/Domain/CalculatorEngine.swift b/Calculator/Calculator/Domain/CalculatorEngine.swift new file mode 100644 index 0000000..eaccb26 --- /dev/null +++ b/Calculator/Calculator/Domain/CalculatorEngine.swift @@ -0,0 +1,11 @@ +// +// CalculatorEngine.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +public protocol CalculatorEngine { + func reduce(state: CalculatorState, action: CalculatorAction) -> CalculatorState +} \ No newline at end of file diff --git a/Calculator/Calculator/Domain/CalculatorTypes.swift b/Calculator/Calculator/Domain/CalculatorTypes.swift new file mode 100644 index 0000000..0cde42d --- /dev/null +++ b/Calculator/Calculator/Domain/CalculatorTypes.swift @@ -0,0 +1,40 @@ +// +// CalculatorTypes.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import Foundation + +public enum Operation: Equatable { + case add, sub, mul, div +} + +public enum CalculatorAction: Equatable { + case digit(Int) + case dot + case op(Operation) + case equals + case clear + case delete + case toggleSign + case percent +} + +public struct CalculatorState: Equatable { + public var display: String = "0" + + public var entry: String = "0" + + public var accumulator: Decimal? = nil + + public var pendingOp: Operation? = nil + + public var isEnteringNewNumber: Bool = true + + public var error: String? = nil + + public init() {} +} diff --git a/Calculator/Calculator/Domain/DefaultCalculatorEngine.swift b/Calculator/Calculator/Domain/DefaultCalculatorEngine.swift new file mode 100644 index 0000000..85692df --- /dev/null +++ b/Calculator/Calculator/Domain/DefaultCalculatorEngine.swift @@ -0,0 +1,169 @@ +// +// DefaultCalculatorEngine.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import Foundation + +public struct DefaultCalculatorEngine: CalculatorEngine { + public init() {} + + public func reduce(state: CalculatorState, action: CalculatorAction) -> CalculatorState { + var s = state + s.error = nil + + switch action { + case .clear: + return CalculatorState() + + case .digit(let n): + return handleDigit(&s, n) + + case .dot: + return handleDot(&s) + + case .delete: + return handleDelete(&s) + + case .toggleSign: + return handleToggleSign(&s) + + case .percent: + return handlePercent(&s) + + case .op(let op): + return handleOp(&s, op) + + case .equals: + return handleEquals(&s) + } + } +} + +private extension DefaultCalculatorEngine { + + func handleDigit(_ s: inout CalculatorState, _ n: Int) -> CalculatorState { + guard (0...9).contains(n) else { return s } + + if s.isEnteringNewNumber { + s.entry = "\(n)" + s.isEnteringNewNumber = false + } else { + if s.entry == "0" { s.entry = "\(n)" } + else if s.entry == "-0" { s.entry = "-\(n)" } + else { s.entry += "\(n)" } + } + s.display = s.entry + return s + } + + func handleDot(_ s: inout CalculatorState) -> CalculatorState { + if s.isEnteringNewNumber { + s.entry = "0." + s.isEnteringNewNumber = false + } else if !s.entry.contains(".") { + s.entry += "." + } + s.display = s.entry + return s + } + + func handleDelete(_ s: inout CalculatorState) -> CalculatorState { + guard !s.isEnteringNewNumber else { return s } + + if s.entry.count <= 1 || (s.entry.count == 2 && s.entry.hasPrefix("-")) { + s.entry = "0" + s.isEnteringNewNumber = true + } else { + s.entry.removeLast() + if s.entry.last == "." { s.entry.removeLast() } // "12." 방지 + if s.entry == "-" { s.entry = "0"; s.isEnteringNewNumber = true } + } + s.display = s.entry + return s + } + + func handleToggleSign(_ s: inout CalculatorState) -> CalculatorState { + if s.entry == "0" { return s } + if s.entry.hasPrefix("-") { s.entry.removeFirst() } + else { s.entry = "-" + s.entry } + s.display = s.entry + return s + } + + func handlePercent(_ s: inout CalculatorState) -> CalculatorState { + guard let v = Decimal(string: s.entry) else { return s } + let r = v / 100 + s.entry = format(r) + s.display = s.entry + return s + } + + func handleOp(_ s: inout CalculatorState, _ op: Operation) -> CalculatorState { + guard let current = Decimal(string: s.entry) else { return s } + + if s.accumulator == nil { + s.accumulator = current + } else if let acc = s.accumulator, let pending = s.pendingOp, !s.isEnteringNewNumber { + guard let result = apply(pending, acc, current) else { return setError(&s) } + s.accumulator = result + s.entry = format(result) + s.display = s.entry + } else { + + } + + s.pendingOp = op + s.isEnteringNewNumber = true + return s + } + + func handleEquals(_ s: inout CalculatorState) -> CalculatorState { + guard let acc = s.accumulator, let pending = s.pendingOp else { + s.display = s.entry + s.isEnteringNewNumber = true + return s + } + guard let current = Decimal(string: s.entry) else { return s } + + guard let result = apply(pending, acc, current) else { return setError(&s) } + s.accumulator = result + s.pendingOp = nil + s.entry = format(result) + s.display = s.entry + s.isEnteringNewNumber = true + return s + } + + func apply(_ op: Operation, _ a: Decimal, _ b: Decimal) -> Decimal? { + switch op { + case .add: return a + b + case .sub: return a - b + case .mul: return a * b + case .div: + if b == 0 { return nil } + return a / b + } + } + + func setError(_ s: inout CalculatorState) -> CalculatorState { + s.error = "Error" + s.display = "Error" + s.entry = "0" + s.accumulator = nil + s.pendingOp = nil + s.isEnteringNewNumber = true + return s + } + + func format(_ d: Decimal) -> String { + let ns = d as NSDecimalNumber + var str = ns.stringValue + + if str == "-0" { str = "0" } + return str + } +} diff --git a/Calculator/Calculator/Presentation/CalculatorViewModel.swift b/Calculator/Calculator/Presentation/CalculatorViewModel.swift new file mode 100644 index 0000000..ced4db4 --- /dev/null +++ b/Calculator/Calculator/Presentation/CalculatorViewModel.swift @@ -0,0 +1,28 @@ +// +// CalculatorViewModel.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import SwiftUI + +import Combine + + +@MainActor +final class CalculatorViewModel: ObservableObject { + @Published private(set) var state: CalculatorState + + private let engine: CalculatorEngine + + init(engine: CalculatorEngine, initial: CalculatorState = .init()) { + self.engine = engine + self.state = initial + } + + func send(_ action: CalculatorAction) { + state = engine.reduce(state: state, action: action) + } +} diff --git a/Calculator/Calculator/UI/CalculatorButton.swift b/Calculator/Calculator/UI/CalculatorButton.swift new file mode 100644 index 0000000..0fa4fa1 --- /dev/null +++ b/Calculator/Calculator/UI/CalculatorButton.swift @@ -0,0 +1,68 @@ +// +// CalculatorButton.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import Foundation + +enum CalcButton: Equatable { + case clear, toggleSign, percent, delete + case op(Operation) + case digit(Int) + case dot + case equals + + var title: String { + switch self { + case .clear: return "C" + case .toggleSign: return "±" + case .percent: return "%" + case .delete: return "⌫" + case .op(let op): + switch op { + case .add: return "+" + case .sub: return "−" + case .mul: return "×" + case .div: return "÷" + } + case .digit(let n): return "\(n)" + case .dot: return "." + case .equals: return "=" + } + } + + var a11yId: String { + switch self { + case .clear: return "btn_clear" + case .toggleSign: return "btn_toggleSign" + case .percent: return "btn_percent" + case .delete: return "btn_delete" + case .op(let op): + switch op { + case .add: return "btn_add" + case .sub: return "btn_sub" + case .mul: return "btn_mul" + case .div: return "btn_div" + } + case .digit(let n): return "btn_\(n)" + case .dot: return "btn_dot" + case .equals: return "btn_equals" + } + } + + func toAction() -> CalculatorAction { + switch self { + case .clear: return .clear + case .toggleSign: return .toggleSign + case .percent: return .percent + case .delete: return .delete + case .op(let op): return .op(op) + case .digit(let n): return .digit(n) + case .dot: return .dot + case .equals: return .equals + } + } +} diff --git a/Calculator/Calculator/UI/CalculatorKeyStyle.swift b/Calculator/Calculator/UI/CalculatorKeyStyle.swift new file mode 100644 index 0000000..f29410c --- /dev/null +++ b/Calculator/Calculator/UI/CalculatorKeyStyle.swift @@ -0,0 +1,30 @@ +// +// CalculatorKeyStyle.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import SwiftUI + +struct CalculatorKeyStyle: ButtonStyle { + let isOperator: Bool + let isFunction: Bool + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(size: 28, weight: .semibold)) + .frame(maxWidth: .infinity, minHeight: 56) + .foregroundStyle(.white) + .background(backgroundColor(configuration.isPressed)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .scaleEffect(configuration.isPressed ? 0.98 : 1.0) + } + + private func backgroundColor(_ pressed: Bool) -> Color { + if isOperator { return pressed ? .orange.opacity(0.85) : .orange } + if isFunction { return pressed ? .gray.opacity(0.85) : .gray } + return pressed ? .gray.opacity(0.7) : .gray.opacity(0.55) + } +} \ No newline at end of file diff --git a/Calculator/Calculator/UI/CalculatorView.swift b/Calculator/Calculator/UI/CalculatorView.swift new file mode 100644 index 0000000..0563d07 --- /dev/null +++ b/Calculator/Calculator/UI/CalculatorView.swift @@ -0,0 +1,81 @@ +// +// CalculatorView.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import SwiftUI + +struct CalculatorView: View { + @StateObject var viewModel: CalculatorViewModel + + private let columns = Array(repeating: GridItem(.flexible(), spacing: 10), count: 4) + + private let keys: [[CalcButton]] = [ + [.clear, .toggleSign, .percent, .delete], + [.digit(7), .digit(8), .digit(9), .op(.div)], + [.digit(4), .digit(5), .digit(6), .op(.mul)], + [.digit(1), .digit(2), .digit(3), .op(.sub)], + [.digit(0), .dot, .equals, .op(.add)] + ] + + var body: some View { + VStack(spacing: 16) { + displayArea + + LazyVGrid(columns: columns, spacing: 10) { + ForEach(keys.flatMap { $0 }, id: \.a11yId) { key in + Button { + viewModel.send(key.toAction()) + } label: { + Text(key.title) + .frame(maxWidth: .infinity) + } + .buttonStyle(CalculatorKeyStyle( + isOperator: isOperator(key), + isFunction: isFunction(key) + )) + .accessibilityIdentifier(key.a11yId) + } + } + } + .padding() + .background(Color.black.ignoresSafeArea()) + } + + private var displayArea: some View { + VStack(alignment: .trailing, spacing: 6) { + if let err = viewModel.state.error { + Text(err) + .font(.system(size: 16, weight: .medium)) + .foregroundStyle(.red) + .frame(maxWidth: .infinity, alignment: .trailing) + } + + Text(viewModel.state.display) + .font(.system(size: 52, weight: .semibold)) + .foregroundStyle(.white) + .minimumScaleFactor(0.5) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .trailing) + .accessibilityIdentifier("display") + } + .padding(.vertical, 10) + .padding(.horizontal, 6) + } + + private func isOperator(_ k: CalcButton) -> Bool { + if case .op = k { return true } + if case .equals = k { return true } + return false + } + + private func isFunction(_ k: CalcButton) -> Bool { + switch k { + case .clear, .toggleSign, .percent, .delete: return true + default: return false + } + } +} \ No newline at end of file diff --git a/Calculator/CalculatorTests/CalculatorEngineTests.swift b/Calculator/CalculatorTests/CalculatorEngineTests.swift new file mode 100644 index 0000000..269aacb --- /dev/null +++ b/Calculator/CalculatorTests/CalculatorEngineTests.swift @@ -0,0 +1,99 @@ +// +// CalculatorEngineTests.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import XCTest + +@testable import Calculator + +final class CalculatorEngineTests: XCTestCase { + + func test_1_plus_2_equals_3() { + let engine = DefaultCalculatorEngine() + var s = CalculatorState() + + s = engine.reduce(state: s, action: .digit(1)) + s = engine.reduce(state: s, action: .op(.add)) + s = engine.reduce(state: s, action: .digit(2)) + s = engine.reduce(state: s, action: .equals) + + XCTAssertEqual(s.display, "3") + } + + func test_leftToRight_2_plus_3_mul_4_equals_20() { + let engine = DefaultCalculatorEngine() + var s = CalculatorState() + + s = engine.reduce(state: s, action: .digit(2)) + s = engine.reduce(state: s, action: .op(.add)) + s = engine.reduce(state: s, action: .digit(3)) + s = engine.reduce(state: s, action: .op(.mul)) // 즉시 계산으로 여기서 5가 됨 + XCTAssertEqual(s.display, "5") + + s = engine.reduce(state: s, action: .digit(4)) + s = engine.reduce(state: s, action: .equals) + XCTAssertEqual(s.display, "20") + } + + func test_operatorReplacement_whenOpTappedTwice_onlyPendingChanges() { + let engine = DefaultCalculatorEngine() + var s = CalculatorState() + + s = engine.reduce(state: s, action: .digit(9)) + s = engine.reduce(state: s, action: .op(.add)) + s = engine.reduce(state: s, action: .op(.mul)) // 숫자 입력 없이 op만 교체 + s = engine.reduce(state: s, action: .digit(2)) + s = engine.reduce(state: s, action: .equals) + + XCTAssertEqual(s.display, "18") // 9 * 2 + } + + func test_divideByZero_showsError() { + let engine = DefaultCalculatorEngine() + var s = CalculatorState() + + s = engine.reduce(state: s, action: .digit(8)) + s = engine.reduce(state: s, action: .op(.div)) + s = engine.reduce(state: s, action: .digit(0)) + s = engine.reduce(state: s, action: .equals) + + XCTAssertEqual(s.display, "Error") + XCTAssertEqual(s.error, "Error") + } + + func test_dotInput() { + let engine = DefaultCalculatorEngine() + var s = CalculatorState() + + s = engine.reduce(state: s, action: .digit(1)) + s = engine.reduce(state: s, action: .dot) + s = engine.reduce(state: s, action: .digit(5)) + + XCTAssertEqual(s.display, "1.5") + } + + func test_toggleSign() { + let engine = DefaultCalculatorEngine() + var s = CalculatorState() + + s = engine.reduce(state: s, action: .digit(9)) + s = engine.reduce(state: s, action: .toggleSign) + + XCTAssertEqual(s.display, "-9") + } + + func test_percent() { + let engine = DefaultCalculatorEngine() + var s = CalculatorState() + + s = engine.reduce(state: s, action: .digit(5)) + s = engine.reduce(state: s, action: .digit(0)) + s = engine.reduce(state: s, action: .percent) + + XCTAssertEqual(s.display, "0.5") + } +} diff --git a/Calculator/CalculatorTests/CalculatorUITests.swift b/Calculator/CalculatorTests/CalculatorUITests.swift new file mode 100644 index 0000000..5818a49 --- /dev/null +++ b/Calculator/CalculatorTests/CalculatorUITests.swift @@ -0,0 +1,37 @@ +// +// CalculatorUITests.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + +import XCTest + +@testable import Calculator + +final class CalculatorUITests: XCTestCase { + + func test_1_plus_2_equals_3() { + let app = XCUIApplication() + app.launch() + + app.buttons["btn_1"].tap() + app.buttons["btn_add"].tap() + app.buttons["btn_2"].tap() + app.buttons["btn_equals"].tap() + + XCTAssertEqual(app.staticTexts["display"].label, "3") + } + + func test_divideByZero_showsError() { + let app = XCUIApplication() + app.launch() + + app.buttons["btn_8"].tap() + app.buttons["btn_div"].tap() + app.buttons["btn_0"].tap() + app.buttons["btn_equals"].tap() + + XCTAssertEqual(app.staticTexts["display"].label, "Error") + } +} diff --git a/Calculator/CalculatorTests/CalculatorViewModelTests.swift b/Calculator/CalculatorTests/CalculatorViewModelTests.swift new file mode 100644 index 0000000..c5acd39 --- /dev/null +++ b/Calculator/CalculatorTests/CalculatorViewModelTests.swift @@ -0,0 +1,29 @@ +// +// CalculatorViewModelTests.swift +// Calculator +// +// Created by 안치욱 on 12/26/25. +// + + +import XCTest + +@testable import Calculator + +private struct StubEngine: CalculatorEngine { + func reduce(state: CalculatorState, action: CalculatorAction) -> CalculatorState { + var s = state + s.display = "stub" + return s + } +} + +@MainActor +final class CalculatorViewModelTests: XCTestCase { + + func test_send_updatesPublishedState() { + let vm = CalculatorViewModel(engine: StubEngine()) + vm.send(.digit(1)) + XCTAssertEqual(vm.state.display, "stub") + } +}