diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2995c9bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,cocoapods +# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,cocoapods + +### CocoaPods ### +## CocoaPods GitIgnore Template + +# CocoaPods - Only use to conserve bandwidth / Save time on Pushing +# - Also handy if you have a large number of dependant pods +# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE +Pods/ + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Xcode ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + + +### DS_Store ### +.DS_Store + + +## Gcc Patch +/*.gcno + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/xcode,swift,cocoapods diff --git a/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.pbxproj b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.pbxproj new file mode 100644 index 00000000..21c9a90d --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.pbxproj @@ -0,0 +1,450 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 226E5C092B47C319005D3DF5 /* ContactDetailCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226E5C082B47C319005D3DF5 /* ContactDetailCell.swift */; }; + 226E5C0B2B47CAAB005D3DF5 /* ContactManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226E5C0A2B47CAAB005D3DF5 /* ContactManager.swift */; }; + 22C842C12B4FCE2600279FDB /* Verification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C842C02B4FCE2600279FDB /* Verification.swift */; }; + 4E1950EF2B439D9100031A6B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1950EE2B439D9100031A6B /* AppDelegate.swift */; }; + 4E1950F12B439D9100031A6B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1950F02B439D9100031A6B /* SceneDelegate.swift */; }; + 4E1950F32B439D9100031A6B /* ContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1950F22B439D9100031A6B /* ContactViewController.swift */; }; + 4E1950F82B439D9300031A6B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4E1950F72B439D9300031A6B /* Assets.xcassets */; }; + 4E1950FB2B439D9300031A6B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4E1950F92B439D9300031A6B /* LaunchScreen.storyboard */; }; + 4E43646B2B47BE55002E5D43 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E43646A2B47BE55002E5D43 /* Contact.swift */; }; + 4E4364A42B4BA9EC002E5D43 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4364A32B4BA9EC002E5D43 /* Error.swift */; }; + 4E4364A62B4BAC57002E5D43 /* AssetDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4364A52B4BAC57002E5D43 /* AssetDecoder.swift */; }; + 4E96035B2B4BD7AE00DC7DD6 /* ViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E96035A2B4BD7AE00DC7DD6 /* ViewController+.swift */; }; + 4E9603702B4FBFF600DC7DD6 /* ContactDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E96036F2B4FBFF600DC7DD6 /* ContactDetailViewController.swift */; }; + 4E9603722B4FC0E600DC7DD6 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9603712B4FC0E600DC7DD6 /* DetailView.swift */; }; + 4E9603742B4FDC8900DC7DD6 /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9603732B4FDC8900DC7DD6 /* String+.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 226E5C082B47C319005D3DF5 /* ContactDetailCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailCell.swift; sourceTree = ""; }; + 226E5C0A2B47CAAB005D3DF5 /* ContactManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactManager.swift; sourceTree = ""; }; + 22C842C02B4FCE2600279FDB /* Verification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Verification.swift; sourceTree = ""; }; + 4E1950EB2B439D9100031A6B /* ios-contact-manager-ui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ios-contact-manager-ui.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4E1950EE2B439D9100031A6B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 4E1950F02B439D9100031A6B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 4E1950F22B439D9100031A6B /* ContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewController.swift; sourceTree = ""; }; + 4E1950F72B439D9300031A6B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4E1950FA2B439D9300031A6B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 4E1950FC2B439D9300031A6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4E43646A2B47BE55002E5D43 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; + 4E4364A32B4BA9EC002E5D43 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + 4E4364A52B4BAC57002E5D43 /* AssetDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetDecoder.swift; sourceTree = ""; }; + 4E96035A2B4BD7AE00DC7DD6 /* ViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+.swift"; sourceTree = ""; }; + 4E96036F2B4FBFF600DC7DD6 /* ContactDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailViewController.swift; sourceTree = ""; }; + 4E9603712B4FC0E600DC7DD6 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; + 4E9603732B4FDC8900DC7DD6 /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4E1950E82B439D9100031A6B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 22595DDE2B50D76600D72D77 /* Extensions */ = { + isa = PBXGroup; + children = ( + 4E96035A2B4BD7AE00DC7DD6 /* ViewController+.swift */, + 4E9603732B4FDC8900DC7DD6 /* String+.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 2290493F2B47265B001E938F /* Views */ = { + isa = PBXGroup; + children = ( + 226E5C082B47C319005D3DF5 /* ContactDetailCell.swift */, + 4E9603712B4FC0E600DC7DD6 /* DetailView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 229049402B472665001E938F /* Controllers */ = { + isa = PBXGroup; + children = ( + 4E1950F22B439D9100031A6B /* ContactViewController.swift */, + 4E96036F2B4FBFF600DC7DD6 /* ContactDetailViewController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 22C842BD2B4FBD5D00279FDB /* Sources */ = { + isa = PBXGroup; + children = ( + 4E4364692B47BE1E002E5D43 /* Models */, + 2290493F2B47265B001E938F /* Views */, + 229049402B472665001E938F /* Controllers */, + ); + path = Sources; + sourceTree = ""; + }; + 22C842BE2B4FBD6D00279FDB /* Resources */ = { + isa = PBXGroup; + children = ( + 22595DDE2B50D76600D72D77 /* Extensions */, + 22C842BF2B4FBDA200279FDB /* Storyboards */, + 4E1950EE2B439D9100031A6B /* AppDelegate.swift */, + 4E1950F02B439D9100031A6B /* SceneDelegate.swift */, + 4E1950F72B439D9300031A6B /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + 22C842BF2B4FBDA200279FDB /* Storyboards */ = { + isa = PBXGroup; + children = ( + 4E1950F92B439D9300031A6B /* LaunchScreen.storyboard */, + ); + path = Storyboards; + sourceTree = ""; + }; + 4E1950E22B439D9100031A6B = { + isa = PBXGroup; + children = ( + 4E1950ED2B439D9100031A6B /* ios-contact-manager-ui */, + 4E1950EC2B439D9100031A6B /* Products */, + ); + sourceTree = ""; + }; + 4E1950EC2B439D9100031A6B /* Products */ = { + isa = PBXGroup; + children = ( + 4E1950EB2B439D9100031A6B /* ios-contact-manager-ui.app */, + ); + name = Products; + sourceTree = ""; + }; + 4E1950ED2B439D9100031A6B /* ios-contact-manager-ui */ = { + isa = PBXGroup; + children = ( + 22C842BE2B4FBD6D00279FDB /* Resources */, + 22C842BD2B4FBD5D00279FDB /* Sources */, + 4E1950FC2B439D9300031A6B /* Info.plist */, + ); + path = "ios-contact-manager-ui"; + sourceTree = ""; + }; + 4E4364692B47BE1E002E5D43 /* Models */ = { + isa = PBXGroup; + children = ( + 4E43646A2B47BE55002E5D43 /* Contact.swift */, + 226E5C0A2B47CAAB005D3DF5 /* ContactManager.swift */, + 4E4364A52B4BAC57002E5D43 /* AssetDecoder.swift */, + 4E4364A32B4BA9EC002E5D43 /* Error.swift */, + 22C842C02B4FCE2600279FDB /* Verification.swift */, + ); + path = Models; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4E1950EA2B439D9100031A6B /* ios-contact-manager-ui */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4E1950FF2B439D9300031A6B /* Build configuration list for PBXNativeTarget "ios-contact-manager-ui" */; + buildPhases = ( + 4E1950E72B439D9100031A6B /* Sources */, + 4E1950E82B439D9100031A6B /* Frameworks */, + 4E1950E92B439D9100031A6B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ios-contact-manager-ui"; + productName = "ios-contact-manager-ui"; + productReference = 4E1950EB2B439D9100031A6B /* ios-contact-manager-ui.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4E1950E32B439D9100031A6B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + 4E1950EA2B439D9100031A6B = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 4E1950E62B439D9100031A6B /* Build configuration list for PBXProject "ios-contact-manager-ui" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4E1950E22B439D9100031A6B; + productRefGroup = 4E1950EC2B439D9100031A6B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4E1950EA2B439D9100031A6B /* ios-contact-manager-ui */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4E1950E92B439D9100031A6B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E1950FB2B439D9300031A6B /* LaunchScreen.storyboard in Resources */, + 4E1950F82B439D9300031A6B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4E1950E72B439D9100031A6B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22C842C12B4FCE2600279FDB /* Verification.swift in Sources */, + 4E43646B2B47BE55002E5D43 /* Contact.swift in Sources */, + 4E1950F32B439D9100031A6B /* ContactViewController.swift in Sources */, + 226E5C092B47C319005D3DF5 /* ContactDetailCell.swift in Sources */, + 4E96035B2B4BD7AE00DC7DD6 /* ViewController+.swift in Sources */, + 4E4364A62B4BAC57002E5D43 /* AssetDecoder.swift in Sources */, + 4E9603722B4FC0E600DC7DD6 /* DetailView.swift in Sources */, + 4E1950EF2B439D9100031A6B /* AppDelegate.swift in Sources */, + 4E9603702B4FBFF600DC7DD6 /* ContactDetailViewController.swift in Sources */, + 226E5C0B2B47CAAB005D3DF5 /* ContactManager.swift in Sources */, + 4E9603742B4FDC8900DC7DD6 /* String+.swift in Sources */, + 4E4364A42B4BA9EC002E5D43 /* Error.swift in Sources */, + 4E1950F12B439D9100031A6B /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 4E1950F92B439D9300031A6B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4E1950FA2B439D9300031A6B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4E1950FD2B439D9300031A6B /* 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 = 17.0; + 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; + }; + 4E1950FE2B439D9300031A6B /* 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 = 17.0; + 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; + }; + 4E1951002B439D9300031A6B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = SZ6T22HKJ4; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "ios-contact-manager-ui/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + 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 = "mireu-contact-manager-ui"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4E1951012B439D9300031A6B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = SZ6T22HKJ4; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "ios-contact-manager-ui/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + 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 = "mireu-contact-manager-ui"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4E1950E62B439D9100031A6B /* Build configuration list for PBXProject "ios-contact-manager-ui" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4E1950FD2B439D9300031A6B /* Debug */, + 4E1950FE2B439D9300031A6B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4E1950FF2B439D9300031A6B /* Build configuration list for PBXNativeTarget "ios-contact-manager-ui" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4E1951002B439D9300031A6B /* Debug */, + 4E1951012B439D9300031A6B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4E1950E32B439D9100031A6B /* Project object */; +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/xcuserdata/leeminyeol.xcuserdatad/UserInterfaceState.xcuserstate b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/xcuserdata/leeminyeol.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..129118ce Binary files /dev/null and b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/project.xcworkspace/xcuserdata/leeminyeol.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/xcuserdata/dj.xcuserdatad/xcschemes/xcschememanagement.plist b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/xcuserdata/dj.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..57435eb2 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/xcuserdata/dj.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + ios-contact-manager-ui.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/xcuserdata/leeminyeol.xcuserdatad/xcschemes/xcschememanagement.plist b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/xcuserdata/leeminyeol.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..57435eb2 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui.xcodeproj/xcuserdata/leeminyeol.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + ios-contact-manager-ui.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/AppDelegate.swift b/ios-contact-manager-ui/ios-contact-manager-ui/AppDelegate.swift new file mode 100644 index 00000000..c7d51275 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/AppDelegate.swift @@ -0,0 +1,28 @@ +// +// AppDelegate.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/2/24. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { } + +} + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/AccentColor.colorset/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/MOCK_DATA.dataset/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/MOCK_DATA.dataset/Contents.json new file mode 100644 index 00000000..eac9de5a --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/MOCK_DATA.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "MOCK_DATA.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/MOCK_DATA.dataset/MOCK_DATA.json b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/MOCK_DATA.dataset/MOCK_DATA.json new file mode 100644 index 00000000..44086634 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Assets.xcassets/MOCK_DATA.dataset/MOCK_DATA.json @@ -0,0 +1,15 @@ +[{"id":"f3fbce0c-7f3a-4e92-935f-1da375975856","name":"Anstice Corsan","age":"1","phoneNumber":"+82 66-877-9432"}, +{"id":"310c81a7-06bd-4ec6-8421-4567a5f1e3a6","name":"Gal Roma","age":"2","phoneNumber":"+82 81-700-9762"}, +{"id":"a842932b-90c1-46ba-88a9-e2ef48b34a36","name":"Joellen Simmon","age":"3","phoneNumber":"+82 84-411-2101"}, +{"id":"63c47198-1fa3-4a5f-b595-59e21473ec3b","name":"Lazar Hoodspeth","age":"4","phoneNumber":"+82 72-977-7004"}, +{"id":"eedd5123-406f-45b6-bbf0-f64093c5265f","name":"Nan Chaffey","age":"5","phoneNumber":"+82 78-512-3245"}, +{"id":"31ec410f-7802-41ae-b390-5dc13cfba056","name":"Carlo Abramovitch","age":"6","phoneNumber":"+82 20-412-6492"}, +{"id":"5485caac-ea78-45b0-a740-f10d4249e2c1","name":"George Kroch","age":"7","phoneNumber":"+82 01-213-0967"}, +{"id":"5b16d43a-5f70-4540-9375-117fe1a34373","name":"Karole Dunsire","age":"8","phoneNumber":"+82 07-265-4369"}, +{"id":"83054610-81b0-4038-a283-5007dc67e643","name":"Andy Wyburn","age":"9","phoneNumber":"+82 58-325-6456"}, +{"id":"68b620e5-9f18-4fd1-94cc-8f2d562c4841","name":"Etan Exroll","age":"10","phoneNumber":"+82 97-920-2887"}, +{"id":"d05f8d47-42a4-4b25-851d-6f18a1d5a05a","name":"Abagail Caulton","age":"11","phoneNumber":"+82 38-717-0061"}, +{"id":"755ba97e-103e-4aab-a8b7-b0a18342cf35","name":"Beatrisa Grogor","age":"12","phoneNumber":"+82 85-614-8629"}, +{"id":"e46de0d9-f323-48d8-93e4-d1d75993386c","name":"Nancee Muckleston","age":"13","phoneNumber":"+82 97-849-9146"}, +{"id":"ada8b4c5-ddd0-485d-ba6e-62824286c051","name":"Harriette Crush","age":"14","phoneNumber":"+82 48-103-3915"}, +{"id":"483958c4-f036-4a70-b9bf-d1b1b6cd5156","name":"Randi Hearty","age":"15","phoneNumber":"+82 50-613-8931"}] diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Controller/ContactViewController.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Controller/ContactViewController.swift new file mode 100644 index 00000000..d094be61 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Controller/ContactViewController.swift @@ -0,0 +1,81 @@ +// +// ViewController.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/2/24. +// + +import UIKit + +final class ContactViewController: UIViewController { + + private let tableView: UITableView = { + let view = UITableView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private var contacts: [Contact] = [] + + override func viewDidLoad() { + super.viewDidLoad() + layout() + parse() + configure() + } + + private func layout() { + view.addSubview(tableView) + + NSLayoutConstraint.activate([tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0)]) + } + + private func configure() { + tableView.dataSource = self + tableView.delegate = self + tableView.register(CustomCell.self, forCellReuseIdentifier: CustomCell.identifier) + } + + private func parse() { + do { + contacts = try AssetDecoder<[Contact]>().parse(assetName: "MOCK_DATA") + } catch { + showErrorAlert(error) + + } + } +} + +// MARK: - UITableViewDataSource + +extension ContactViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return contacts.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.identifier , for: indexPath) + let item = contacts[indexPath.row] + + var content = cell.defaultContentConfiguration() + + content.text = item.nameAndAge + content.secondaryText = item.phoneNumber + + cell.contentConfiguration = content + cell.accessoryType = .disclosureIndicator + + return cell + } +} + +// MARK: - UITableViewDelegate + +extension ContactViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Controller/ViewController+.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Controller/ViewController+.swift new file mode 100644 index 00000000..a84b27ec --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Controller/ViewController+.swift @@ -0,0 +1,37 @@ +// +// ViewController.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/8/24. +// + +import UIKit + +extension UIViewController { + func showErrorAlert(_ error: Error) { + let alertTitle: String = "오류" + var message: String { + switch error { + case DecoderError.assetName: + return "에셋네임을 알수 없습니다." + case DecoderError.jsonData: + return "데이터를 알수 없습니다." + default: + return "시스템오류가 발생했습니다." + } + } + + let cancelTiltle: String = "취소" + let retryTitle: String = "재시도" + + let alert = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert) + + let cancelAction = UIAlertAction(title: cancelTiltle, style: .default) + let retryAction = UIAlertAction(title: retryTitle, style: .default) + + alert.addAction(retryAction) + alert.addAction(cancelAction) + + self.present(alert, animated: true) + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Info.plist b/ios-contact-manager-ui/ios-contact-manager-ui/Info.plist new file mode 100644 index 00000000..0eb786dc --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Info.plist @@ -0,0 +1,23 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Model/AssetDecoder.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Model/AssetDecoder.swift new file mode 100644 index 00000000..d099104d --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Model/AssetDecoder.swift @@ -0,0 +1,20 @@ +// +// AssetDecoder.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/8/24. +// + +import Foundation +import UIKit + +struct AssetDecoder { + func parse(assetName: String) throws -> Element { + + guard let asset = NSDataAsset(name: assetName) else { throw DecoderError.assetName } + + guard let jsonData = try? JSONDecoder().decode(Element.self, from: asset.data) else { throw DecoderError.jsonData } + + return jsonData + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Model/Contact.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Model/Contact.swift new file mode 100644 index 00000000..c0734bda --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Model/Contact.swift @@ -0,0 +1,19 @@ +// +// Contact.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/5/24. +// + +import Foundation + +struct Contact: Decodable { + var id = UUID() + let name: String + let phoneNumber: String + let age: String + + var nameAndAge: String { + return "\(name)(\(age))" + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Model/ContactManager.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Model/ContactManager.swift new file mode 100644 index 00000000..9c1fb104 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Model/ContactManager.swift @@ -0,0 +1,32 @@ +// +// ContactManager.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/5/24. +// + +import Foundation +import UIKit + +final class ContactManager { + var contacts: [Contact] = [] + + func add(contact: Contact) { + contacts.append(contact) + } + + func delete(index: IndexPath) { + contacts.remove(at: index.row) + } + + func show(index: IndexPath) -> Contact { + return contacts[index.row] + } + + func update(contact: Contact) { + let contactIndices = contacts.indices + let editContact = contactIndices.filter { contacts[$0].id == contact.id } + editContact.forEach { contacts[$0] = contact } + } +} + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Model/Error.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Model/Error.swift new file mode 100644 index 00000000..f35dd263 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Model/Error.swift @@ -0,0 +1,13 @@ +// +// Error.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/8/24. +// + +import Foundation + +enum DecoderError: Error { + case assetName + case jsonData +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/AppDelegate.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/AppDelegate.swift new file mode 100644 index 00000000..c7d51275 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/AppDelegate.swift @@ -0,0 +1,28 @@ +// +// AppDelegate.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/2/24. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { } + +} + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/MOCK_DATA.dataset/Contents.json b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/MOCK_DATA.dataset/Contents.json new file mode 100644 index 00000000..eac9de5a --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/MOCK_DATA.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "MOCK_DATA.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/MOCK_DATA.dataset/MOCK_DATA.json b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/MOCK_DATA.dataset/MOCK_DATA.json new file mode 100644 index 00000000..44086634 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Assets.xcassets/MOCK_DATA.dataset/MOCK_DATA.json @@ -0,0 +1,15 @@ +[{"id":"f3fbce0c-7f3a-4e92-935f-1da375975856","name":"Anstice Corsan","age":"1","phoneNumber":"+82 66-877-9432"}, +{"id":"310c81a7-06bd-4ec6-8421-4567a5f1e3a6","name":"Gal Roma","age":"2","phoneNumber":"+82 81-700-9762"}, +{"id":"a842932b-90c1-46ba-88a9-e2ef48b34a36","name":"Joellen Simmon","age":"3","phoneNumber":"+82 84-411-2101"}, +{"id":"63c47198-1fa3-4a5f-b595-59e21473ec3b","name":"Lazar Hoodspeth","age":"4","phoneNumber":"+82 72-977-7004"}, +{"id":"eedd5123-406f-45b6-bbf0-f64093c5265f","name":"Nan Chaffey","age":"5","phoneNumber":"+82 78-512-3245"}, +{"id":"31ec410f-7802-41ae-b390-5dc13cfba056","name":"Carlo Abramovitch","age":"6","phoneNumber":"+82 20-412-6492"}, +{"id":"5485caac-ea78-45b0-a740-f10d4249e2c1","name":"George Kroch","age":"7","phoneNumber":"+82 01-213-0967"}, +{"id":"5b16d43a-5f70-4540-9375-117fe1a34373","name":"Karole Dunsire","age":"8","phoneNumber":"+82 07-265-4369"}, +{"id":"83054610-81b0-4038-a283-5007dc67e643","name":"Andy Wyburn","age":"9","phoneNumber":"+82 58-325-6456"}, +{"id":"68b620e5-9f18-4fd1-94cc-8f2d562c4841","name":"Etan Exroll","age":"10","phoneNumber":"+82 97-920-2887"}, +{"id":"d05f8d47-42a4-4b25-851d-6f18a1d5a05a","name":"Abagail Caulton","age":"11","phoneNumber":"+82 38-717-0061"}, +{"id":"755ba97e-103e-4aab-a8b7-b0a18342cf35","name":"Beatrisa Grogor","age":"12","phoneNumber":"+82 85-614-8629"}, +{"id":"e46de0d9-f323-48d8-93e4-d1d75993386c","name":"Nancee Muckleston","age":"13","phoneNumber":"+82 97-849-9146"}, +{"id":"ada8b4c5-ddd0-485d-ba6e-62824286c051","name":"Harriette Crush","age":"14","phoneNumber":"+82 48-103-3915"}, +{"id":"483958c4-f036-4a70-b9bf-d1b1b6cd5156","name":"Randi Hearty","age":"15","phoneNumber":"+82 50-613-8931"}] diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Extensions/String+.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Extensions/String+.swift new file mode 100644 index 00000000..c9fb6f16 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Extensions/String+.swift @@ -0,0 +1,93 @@ +// +// String+.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/11/24. +// + +import Foundation + +extension String { + var removeBlank: String { + return self.replacingOccurrences(of: " ", with: "") + } + + var formmater: String { + var stringWithhypen: String = self + + if stringWithhypen.prefix(4) == "+820" { + switch stringWithhypen.count { + case 4...6: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + case 7...9: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: 6 - count)) + case 10...12: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 7)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: 9 - count)) + case 13...14: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("(", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 4)) + stringWithhypen.insert(")", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 6)) + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 7)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 10)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: -4)) + default: + break + } + } + + else if stringWithhypen.prefix(3) == "+82" { + switch stringWithhypen.count { + case 4...5: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + case 6...8: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 6)) + case 9...11: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 6)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: 8 - count)) + case 12...13: + stringWithhypen.insert(" ", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 6)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: -4)) + default: + break + } + } + + else if stringWithhypen.prefix(1) != "0" || stringWithhypen.prefix(2) == "02" { + switch stringWithhypen.count { + case 3...5: + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 2)) + case 6...8: + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 2)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: 5 - count)) + case 9...10: + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 2)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: -4)) + default: + break + } + } + + else { + switch stringWithhypen.count { + case 4...6: + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + case 7...9: + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: 6 - count)) + case 10...11: + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.startIndex, offsetBy: 3)) + stringWithhypen.insert("-", at: stringWithhypen.index(stringWithhypen.endIndex, offsetBy: -4)) + default: + break + } + } + + return stringWithhypen + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Extensions/ViewController+.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Extensions/ViewController+.swift new file mode 100644 index 00000000..954d142b --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Extensions/ViewController+.swift @@ -0,0 +1,18 @@ +// +// ViewController.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/8/24. +// + +import UIKit + +extension UIViewController { + func showErrorAlert(title: String?, _ message: String, actions: [UIAlertAction]) -> UIAlertController { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + for action in actions { + alert.addAction(action) + } + return alert + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/SceneDelegate.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/SceneDelegate.swift new file mode 100644 index 00000000..9ab78587 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/SceneDelegate.swift @@ -0,0 +1,35 @@ +// +// SceneDelegate.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/2/24. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windwScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windwScene) + + let naviVC = UINavigationController(rootViewController: ContactViewController()) + + window?.rootViewController = naviVC + window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { } + + func sceneDidBecomeActive(_ scene: UIScene) { } + + func sceneWillResignActive(_ scene: UIScene) { } + + func sceneWillEnterForeground(_ scene: UIScene) { } + + func sceneDidEnterBackground(_ scene: UIScene) { } + +} + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/SceneDelegate.swift b/ios-contact-manager-ui/ios-contact-manager-ui/SceneDelegate.swift new file mode 100644 index 00000000..f7718d1b --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/SceneDelegate.swift @@ -0,0 +1,35 @@ +// +// SceneDelegate.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/2/24. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windwScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windwScene) + + let neviVC = UINavigationController(rootViewController: ContactViewController()) + + window?.rootViewController = neviVC + window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { } + + func sceneDidBecomeActive(_ scene: UIScene) { } + + func sceneWillResignActive(_ scene: UIScene) { } + + func sceneWillEnterForeground(_ scene: UIScene) { } + + func sceneDidEnterBackground(_ scene: UIScene) { } + +} + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Controllers/ContactDetailViewController.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Controllers/ContactDetailViewController.swift new file mode 100644 index 00000000..858f9404 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Controllers/ContactDetailViewController.swift @@ -0,0 +1,130 @@ +// +// ContactDetailViewController.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/11/24. +// + +import UIKit + +protocol ContactDetailDelegate: AnyObject { + func add(contact: Contact) + func update(contact: Contact) +} + +final class ContactDetailViewController: UIViewController { + + private let detailView = DetailView() + var contact: Contact? + weak var delegate: ContactDetailDelegate? + + private lazy var cancelButton: UIBarButtonItem = { + let customBtton = UIButton(type: .system) + customBtton.setTitle("Cancel", for: .normal) + customBtton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside) + + let buttonSize = CGSize(width: 15, height: 15) + customBtton.frame = CGRect(origin: .zero, size: buttonSize) + + let button = UIBarButtonItem(customView: customBtton) + + return button + }() + + private lazy var saveButton: UIBarButtonItem = { + let customBtton = UIButton(type: .system) + customBtton.setTitle("Save", for: .normal) + customBtton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside) + + let buttonSize = CGSize(width: 15, height: 15) + customBtton.frame = CGRect(origin: .zero, size: buttonSize) + + let button = UIBarButtonItem(customView: customBtton) + button.isEnabled = false + + return button + }() + + override func loadView() { + view = detailView + } + + override func viewDidLoad() { + super.viewDidLoad() + setupnvBar() + viewTextField() + detailView.contact = contact + } + + private func viewTextField() { + detailView.nameTextField.delegate = self + detailView.ageTextField.delegate = self + detailView.phoneNumberTextField.delegate = self + } + + private func setupnvBar() { + navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15)] + if contact == nil { + title = "새연락처" + navigationItem.leftBarButtonItem = cancelButton + navigationItem.rightBarButtonItem = saveButton + } else { + title = "기존연락처" + navigationItem.leftBarButtonItem = cancelButton + navigationItem.rightBarButtonItem = saveButton + } + } + + @objc private func cancelButtonTapped() { + let alert = showErrorAlert(title: nil, "정말 취소하시겠습니까?", actions: [UIAlertAction(title: "예", style: .cancel, handler: { _ in + self.dismiss(animated: true) + self.navigationController?.popViewController(animated: true) + }), UIAlertAction(title: "아니오", style: .destructive)]) + present(alert, animated: true) + } + + @objc private func saveButtonTapped() { + do { + let (name, age, phone) = try makeInfo() + + if let id = contact?.id { + let editContact = Contact(id: id, name: name, phoneNumber: phone, age: age) + delegate?.update(contact: editContact) + } else { + let newContact = Contact(name: name, phoneNumber: phone, age: age) + delegate?.add(contact: newContact) + } + self.dismiss(animated: true) + } catch { + let alert = showErrorAlert(title: nil, error.localizedDescription, actions: [UIAlertAction(title: "확인", style: .default)]) + present(alert, animated: true) + } + } +} + +// MARK: - Verification + +extension ContactDetailViewController { + func makeInfo() throws -> (String, String, String) { + guard Verification.setName(detailView.nameTextField.text ?? "") || Verification.setAge(detailView.ageTextField.text ?? "") || Verification.setNumber(detailView.phoneNumberTextField.text ?? "") else { throw ContactError.errorAll } + guard let name = detailView.nameTextField.text, Verification.setName(name) else { throw ContactError.errorName } + guard let age = detailView.ageTextField.text, Verification.setAge(age) else { throw ContactError.errorAge } + guard let phone = detailView.phoneNumberTextField.text, Verification.setNumber(phone) else { throw ContactError.errorNumber } + + return (name.removeBlank, age, phone) + } +} + +extension ContactDetailViewController: UITextFieldDelegate { + func textFieldDidChangeSelection(_ textField: UITextField) { + guard let contact = self.contact else { return saveButton.isEnabled = true } + + if detailView.nameTextField.text == contact.name && + detailView.ageTextField.text == contact.age && + detailView.phoneNumberTextField.text == contact.phoneNumber { + saveButton.isEnabled = false + } else { + saveButton.isEnabled = true + } + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Controllers/ContactViewController.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Controllers/ContactViewController.swift new file mode 100644 index 00000000..cdd7163e --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Controllers/ContactViewController.swift @@ -0,0 +1,181 @@ +// +// ViewController.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/2/24. +// + +import UIKit + +final class ContactViewController: UIViewController { + + private lazy var tableView: UITableView = { + let tableView = UITableView() + self.view.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.delegate = self + tableView.dataSource = self + tableView.registerCell(ContactDetailCell.self) + return tableView + }() + + private lazy var plusButton: UIBarButtonItem = { + let customBtton = UIButton(type: .system) + customBtton.setImage(UIImage(systemName: "plus"), for: .normal) + customBtton.addTarget(self, action: #selector(plusButtonTapped), for: .touchUpInside) + + let buttonSize = CGSize(width: 15, height: 15) + customBtton.frame = CGRect(origin: .zero, size: buttonSize) + + let button = UIBarButtonItem(customView: customBtton) + + return button + }() + + private lazy var search: UISearchController = { + let search = UISearchController() + navigationItem.searchController = search + navigationItem.searchController?.searchBar.delegate = self + search.hidesNavigationBarDuringPresentation = false + navigationItem.hidesSearchBarWhenScrolling = false + navigationController?.navigationBar.addSubview(search.searchBar) + search.searchBar.setShowsCancelButton(false, animated: false) + return search + }() + + private let contactManager = ContactManager() + + private var filteredContacts: [Contact] { + return contactManager.contacts.filter { $0.name.contains(search.searchBar.text ?? "") } + } + + override func viewDidLoad() { + super.viewDidLoad() + viewBackground() + layout() + parse() + setupNaviBar() + } + + private func viewBackground() { + view.backgroundColor = .white + } + + private func layout() { + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: self.view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)]) + } + + private func setupNaviBar() { + title = "연락처" + navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15)] + navigationController?.navigationBar.backgroundColor = .white + navigationController?.navigationBar.shadowImage = UIImage() + + navigationItem.rightBarButtonItem = plusButton + } + + private func parse() { + do { + try contactManager.parse() + } catch { + let alert = showErrorAlert(title: nil, error.localizedDescription, actions: [UIAlertAction(title: "취소", style: .default)]) + present(alert, animated: true) + } + } + @objc private func plusButtonTapped() { + let detailVC = ContactDetailViewController() + detailVC.delegate = self + present(UINavigationController(rootViewController: detailVC), animated: true) + } +} + +// MARK: - UITableViewDataSource + +extension ContactViewController: UITableViewDataSource { + private func isNotEmpty(_ name: String) -> Bool { + return name.isEmpty == false + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let name = search.searchBar.text, isNotEmpty(name) { + return filteredContacts.count + } else { + return contactManager.contacts.count + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueCell(ContactDetailCell.self, indexPath: indexPath) + + let item = contactManager.contacts[indexPath.row] + cell.contact = item + + if let name = search.searchBar.text, isNotEmpty(name) { + cell.contact = filteredContacts[indexPath.row] + } else { + cell.contact = contactManager.contacts[indexPath.row] + } + + return cell + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + contactManager.delete(index: indexPath) + tableView.deleteRows(at: [indexPath], with: .fade) + } +} + +// MARK: - UITableViewDelegate + +extension ContactViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let detailVC = ContactDetailViewController() + detailVC.contact = contactManager.contacts[indexPath.row] + detailVC.delegate = self + navigationController?.pushViewController(detailVC, animated: true) + } +} + +// MARK: - ContactDelegate + +extension ContactViewController: ContactDetailDelegate { + func update(contact: Contact) { + contactManager.update(contact) + tableView.reloadData() + } + + func add(contact: Contact) { + contactManager.add(contact) + tableView.reloadData() + } +} + +// MARK: - UISearchBarDelegate + +extension ContactViewController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + tableView.reloadData() + } +} + +extension UITableView { + func registerCell(_ cellType: T.Type) { + let indentifier = String(describing: cellType) + register(cellType, forCellReuseIdentifier: indentifier) + } + + func dequeueCell(_ cellType: T.Type, indexPath: IndexPath) -> T { + let indentifier = String(describing: cellType) + guard let cell = dequeueReusableCell(withIdentifier: indentifier, for: indexPath) as? T else { + return T() + } + return cell + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/AssetDecoder.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/AssetDecoder.swift new file mode 100644 index 00000000..26beaada --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/AssetDecoder.swift @@ -0,0 +1,20 @@ +// +// AssetDecoder.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/8/24. +// + +import Foundation +import UIKit + +struct AssetDecoder { + func parse(assetName: String) throws -> Element { + + guard let asset = NSDataAsset(name: assetName) else { throw ContactError.assetName } + + guard let jsonData = try? JSONDecoder().decode(Element.self, from: asset.data) else { throw ContactError.jsonData } + + return jsonData + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Contact.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Contact.swift new file mode 100644 index 00000000..b32241de --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Contact.swift @@ -0,0 +1,22 @@ +// +// Contact.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/5/24. +// + +import Foundation + +struct Contact: Decodable { + var id = UUID() + var name: String + var phoneNumber: String + var age: String + + init(id: UUID = UUID(), name: String, phoneNumber: String, age: String) { + self.id = id + self.name = name + self.phoneNumber = phoneNumber + self.age = age + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/ContactManager.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/ContactManager.swift new file mode 100644 index 00000000..d46a296b --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/ContactManager.swift @@ -0,0 +1,37 @@ +// +// ContactManager.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/5/24. +// + +import UIKit + +final class ContactManager { + private(set) var contacts: [Contact] = [] + + func add(_ contact: Contact) { + contacts.append(contact) + } + + func delete(index: IndexPath) { + contacts.remove(at: index.row) + } + + func update(_ contact: Contact) { + guard let targetIndex = contacts.firstIndex(where: { $0.id == contact.id }) else { + print("[ContactManager] can not find index") + return + } + contacts[targetIndex] = contact + } + + func parse() throws { + do { + contacts = try AssetDecoder<[Contact]>().parse(assetName: "MOCK_DATA") + } catch { + throw error + } + } +} + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Error.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Error.swift new file mode 100644 index 00000000..1e56e1d1 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Error.swift @@ -0,0 +1,37 @@ +// +// Error.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/8/24. +// + +import Foundation + +enum ContactError: LocalizedError { + case assetName + case jsonData + case errorName + case errorAge + case errorNumber + case errorAll + case systemError + + var errorDescription: String? { + switch self { + case .assetName: + return "에셋네임을 알수 없습니다." + case .jsonData: + return "데이터를 알수 없습니다." + case .errorName: + return "입력한 이름 정보가 잘못되었습니다." + case .errorAge: + return "입력한 나이정보가 잘못되었습니다." + case .errorNumber: + return "입력한 연락처 정보가 잘못되었습니다." + case .errorAll: + return "입력한 정보 모두 올바르지 않습니다." + case .systemError: + return "시스템오류" + } + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Verification.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Verification.swift new file mode 100644 index 00000000..31edc264 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Models/Verification.swift @@ -0,0 +1,26 @@ +// +// Verification.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/11/24. +// + +import Foundation + +struct Verification { + static func setName(_ name: String) -> Bool { + let setName = name.split(separator: " ").reduce("") { $0 + $1 } + let regex = #"^[a-zA-Z]{1,10}\s?([a-zA-Z]{2,10})?$"# + return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: setName) + } + + static func setAge(_ age: String) -> Bool { + let regex = "^([1-9]{1})?([0-9]{1,2})$" + return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: age) + } + + static func setNumber(_ number: String) -> Bool { + let regex = #"(\+[0-9]{2,3}\s?)?(\(0\))?\s?0?([0-9]{1,2})-([0-9]{3,4})-([0-9]{4})$"# + return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: number) + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Views/ContactDetailCell.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Views/ContactDetailCell.swift new file mode 100644 index 00000000..0bba19fd --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Views/ContactDetailCell.swift @@ -0,0 +1,105 @@ +// +// CustomCell.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/5/24. +// + +import UIKit + +final class ContactDetailCell: UITableViewCell { + + var contact: Contact? { + didSet { + guard let contact = contact else { return } + contactNameLabel.text = " 이름 : \(contact.name)" + contactAgeLabel.text = " 나이 : \(contact.age)" + contactPhoneNumberLabel.text = "연락처 : \(contact.phoneNumber)" + + contactNameLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + contactAgeLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + contactPhoneNumberLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + contactNameLabel.adjustsFontForContentSizeCategory = true + contactAgeLabel.adjustsFontForContentSizeCategory = true + contactPhoneNumberLabel.adjustsFontForContentSizeCategory = true + } + } + + let contactNameLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 12) + label.adjustsFontSizeToFitWidth = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let contactAgeLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 15) + label.adjustsFontSizeToFitWidth = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let contactPhoneNumberLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 12) + label.adjustsFontSizeToFitWidth = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .fill + stackView.spacing = 0.2 + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) + setupStackView() + setConstraint() + accessoryType = .disclosureIndicator + } + + func setupStackView() { + self.addSubview(stackView) + + stackView.addArrangedSubview(contactNameLabel) + stackView.addArrangedSubview(contactAgeLabel) + stackView.addArrangedSubview(contactPhoneNumberLabel) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setConstraint() { + NSLayoutConstraint.activate([ + contactNameLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + contactAgeLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + contactPhoneNumberLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + contactNameLabel.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: -20), + contactAgeLabel.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: -20), + contactPhoneNumberLabel.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: -20) + ]) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: self.topAnchor), + stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20), + stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20), + stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) + } +} + +extension UITableViewCell { + static var identifier: String { + return String(describing: self) + } +} + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Views/DetailView.swift b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Views/DetailView.swift new file mode 100644 index 00000000..71fa3f20 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/Sources/Views/DetailView.swift @@ -0,0 +1,190 @@ +// +// DetailView.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/11/24. +// + +import UIKit + +final class DetailView: UIView { + + var contact: Contact? { + didSet { + guard let contact = contact else { return } + nameTextField.text = contact.name + ageTextField.text = contact.age + phoneNumberTextField.text = contact.phoneNumber + } + } + + private let nameLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .callout) + label.text = "이름" + label.adjustsFontSizeToFitWidth = true + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let nameTextField: UITextField = { + let textField = UITextField() + textField.font = UIFont.preferredFont(forTextStyle: .callout) + textField.frame.size.height = 15 + textField.borderStyle = .roundedRect + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.spellCheckingType = .no + textField.adjustsFontForContentSizeCategory = true + textField.adjustsFontSizeToFitWidth = true + textField.clearsOnBeginEditing = false + textField.translatesAutoresizingMaskIntoConstraints = false + return textField + }() + + lazy var nameStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [nameLabel, nameTextField]) + stackView.spacing = 5 + stackView.axis = .horizontal + stackView.distribution = .fill + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private let ageLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .callout) + label.text = "나이" + label.adjustsFontSizeToFitWidth = true + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let ageTextField: UITextField = { + let tf = UITextField() + tf.font = UIFont.preferredFont(forTextStyle: .callout) + tf.frame.size.height = 20 + tf.borderStyle = .roundedRect + tf.autocapitalizationType = .none + tf.autocorrectionType = .no + tf.spellCheckingType = .no + tf.adjustsFontSizeToFitWidth = true + tf.adjustsFontForContentSizeCategory = true + tf.clearsOnBeginEditing = false + tf.translatesAutoresizingMaskIntoConstraints = false + return tf + }() + + lazy var ageStackView: UIStackView = { + let stview = UIStackView(arrangedSubviews: [ageLabel, ageTextField]) + stview.spacing = 5 + stview.axis = .horizontal + stview.distribution = .fill + stview.alignment = .fill + stview.translatesAutoresizingMaskIntoConstraints = false + return stview + }() + + private let phoneNumberLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .callout) + label.text = "전화번호" + label.adjustsFontSizeToFitWidth = true + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let phoneNumberTextField: UITextField = { + let tf = UITextField() + tf.font = UIFont.preferredFont(forTextStyle: .callout) + tf.frame.size.height = 20 + tf.borderStyle = .roundedRect + tf.autocapitalizationType = .none + tf.autocorrectionType = .no + tf.spellCheckingType = .no + tf.adjustsFontSizeToFitWidth = true + tf.adjustsFontForContentSizeCategory = true + tf.clearsOnBeginEditing = false + tf.translatesAutoresizingMaskIntoConstraints = false + return tf + }() + + lazy var phoneNumberStackView: UIStackView = { + let stview = UIStackView(arrangedSubviews: [phoneNumberLabel, phoneNumberTextField]) + stview.spacing = 5 + stview.axis = .horizontal + stview.distribution = .fill + stview.alignment = .fill + stview.translatesAutoresizingMaskIntoConstraints = false + return stview + }() + + lazy var stackView: UIStackView = { + let stview = UIStackView(arrangedSubviews: [nameStackView, ageStackView, phoneNumberStackView]) + stview.spacing = 10 + stview.axis = .vertical + stview.distribution = .fill + stview.alignment = .fill + stview.translatesAutoresizingMaskIntoConstraints = false + return stview + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + setupKeyboardType() + setupStackView() + phoneNumberTextField.addTarget(self, action: #selector(edit), for: .editingChanged) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupKeyboardType() { + nameTextField.keyboardType = .default + ageTextField.keyboardType = .numberPad + phoneNumberTextField.keyboardType = .phonePad + } + + private func setupStackView() { + self.addSubview(stackView) + } + override func updateConstraints() { + setConstraints() + super.updateConstraints() + } + + private func setConstraints() { + + NSLayoutConstraint.activate([ + nameLabel.widthAnchor.constraint(equalToConstant: 70), + ageLabel.widthAnchor.constraint(equalToConstant: 70), + phoneNumberLabel.widthAnchor.constraint(equalToConstant: 70) + ]) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 15), + stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), + stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20) + ]) + } +} + +extension DetailView { + @objc func edit() { + guard let edit = phoneNumberTextField.text?.replacingOccurrences(of: "-", with: "") else { + return + } + + var text = edit.replacingOccurrences(of: " ", with: "") + text = text.replacingOccurrences(of: "(", with: "") + text = text.replacingOccurrences(of: ")", with: "") + + phoneNumberTextField.text = text.formmater + } +} diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/View/Base.lproj/LaunchScreen.storyboard b/ios-contact-manager-ui/ios-contact-manager-ui/View/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/View/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/View/Base.lproj/Main.storyboard b/ios-contact-manager-ui/ios-contact-manager-ui/View/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f7dee370 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/View/Base.lproj/Main.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-contact-manager-ui/ios-contact-manager-ui/View/CustomCell.swift b/ios-contact-manager-ui/ios-contact-manager-ui/View/CustomCell.swift new file mode 100644 index 00000000..dac2e031 --- /dev/null +++ b/ios-contact-manager-ui/ios-contact-manager-ui/View/CustomCell.swift @@ -0,0 +1,14 @@ +// +// CustomCell.swift +// ios-contact-manager-ui +// +// Created by 미르, 루피 on 1/5/24. +// + +import UIKit + +final class CustomCell: UITableViewCell { + static var identifier: String { + return String(describing: self) + } +}