diff --git a/.gitignore b/.gitignore index 330d167..fa79a49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,15 @@ +# Created by https://www.toptal.com/developers/gitignore/api/swiftpackagemanager,swift,cocoapods,xcode +# Edit at https://www.toptal.com/developers/gitignore?templates=swiftpackagemanager,swift,cocoapods,xcode + +### 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 @@ -35,13 +47,11 @@ 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 @@ -49,18 +59,14 @@ playground.xcworkspace .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 @@ -71,7 +77,6 @@ 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: @@ -83,8 +88,28 @@ 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/ + +### SwiftPackageManager ### +Packages +xcuserdata +*.xcodeproj + + +### Xcode ### + +## Xcode 8 and earlier + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/swiftpackagemanager,swift,cocoapods,xcode diff --git a/OpenMarket/OpenMarket.xcodeproj/project.pbxproj b/OpenMarket/OpenMarket.xcodeproj/project.pbxproj index 7b44f3d..d4ee3d9 100644 --- a/OpenMarket/OpenMarket.xcodeproj/project.pbxproj +++ b/OpenMarket/OpenMarket.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 351049722907A4AC00F92E73 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351049712907A4AC00F92E73 /* Error.swift */; }; 351049742907B56E00F92E73 /* Vendor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351049732907B56E00F92E73 /* Vendor.swift */; }; 351049762907B60A00F92E73 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351049752907B60A00F92E73 /* Image.swift */; }; 351049902907C3B100F92E73 /* OpenMarketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3510498F2907C3B100F92E73 /* OpenMarketTests.swift */; }; 3510499A2907C56D00F92E73 /* Mock.json in Resources */ = {isa = PBXBuildFile; fileRef = 351049992907C55F00F92E73 /* Mock.json */; }; + 351414B62910DFAD00ACD6EF /* ListPageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351414B52910DFAD00ACD6EF /* ListPageCell.swift */; }; 353A9D6E2908C1FE00FA2495 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353A9D6D2908C1FE00FA2495 /* Request.swift */; }; 353A9D702908C5E500FA2495 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353A9D6F2908C5E500FA2495 /* Currency.swift */; }; 353A9D722908CAD900FA2495 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353A9D712908CAD900FA2495 /* NetworkManager.swift */; }; @@ -21,15 +21,23 @@ 353A9D782908D76000FA2495 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353A9D712908CAD900FA2495 /* NetworkManager.swift */; }; 353A9D792908D76200FA2495 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353A9D732908CB6600FA2495 /* NetworkError.swift */; }; 353A9D7A2908D76500FA2495 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353A9D752908CD3500FA2495 /* HTTP.swift */; }; + 35828B02291363170047D96D /* ImageCacheable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35828B01291363170047D96D /* ImageCacheable.swift */; }; + 35828B04291364420047D96D /* ImageCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35828B03291364420047D96D /* ImageCacheManager.swift */; }; + 35995AF12914E663007EDD1E /* URLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35995AF02914E663007EDD1E /* URLManager.swift */; }; 35B483BF290F59A60083012D /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35B483BE290F59A60083012D /* Product.swift */; }; + 35BE6CC92911F95F00743A5C /* CustomCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35BE6CC82911F95F00743A5C /* CustomCollectionView.swift */; }; + 35BE6CCB291207DE00743A5C /* CellSelctable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35BE6CCA291207DE00743A5C /* CellSelctable.swift */; }; + 35BE6CCE29120B8600743A5C /* Extension+NSMutableAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35BE6CCD29120B8600743A5C /* Extension+NSMutableAttributedString.swift */; }; C70FB0FB25BEF61C00C9924E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */; }; C70FB0FD25BEF61C00C9924E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */; }; - C70FB0FF25BEF61C00C9924E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FE25BEF61C00C9924E /* ViewController.swift */; }; + C70FB0FF25BEF61C00C9924E /* ProductListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70FB0FE25BEF61C00C9924E /* ProductListViewController.swift */; }; C70FB10225BEF61C00C9924E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C70FB10025BEF61C00C9924E /* Main.storyboard */; }; C70FB10425BEF61D00C9924E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C70FB10325BEF61D00C9924E /* Assets.xcassets */; }; C70FB10725BEF61D00C9924E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C70FB10525BEF61D00C9924E /* LaunchScreen.storyboard */; }; FB07033F29062A55006775BD /* ProductListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB07033E29062A55006775BD /* ProductListPage.swift */; }; FB07034129062E66006775BD /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB07034029062E66006775BD /* Page.swift */; }; + FBC6E025291C841900119301 /* CellIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC6E024291C841900119301 /* CellIdentifier.swift */; }; + FBE9C294291345C4009477FA /* GridPageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBE9C293291345C4009477FA /* GridPageCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,28 +51,36 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 351049712907A4AC00F92E73 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 351049732907B56E00F92E73 /* Vendor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vendor.swift; sourceTree = ""; }; 351049752907B60A00F92E73 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 3510498D2907C3B100F92E73 /* OpenMarketTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenMarketTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3510498F2907C3B100F92E73 /* OpenMarketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenMarketTests.swift; sourceTree = ""; }; 351049992907C55F00F92E73 /* Mock.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Mock.json; sourceTree = ""; }; + 351414B52910DFAD00ACD6EF /* ListPageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListPageCell.swift; sourceTree = ""; }; 353A9D6D2908C1FE00FA2495 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 353A9D6F2908C5E500FA2495 /* Currency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Currency.swift; sourceTree = ""; }; 353A9D712908CAD900FA2495 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 353A9D732908CB6600FA2495 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 353A9D752908CD3500FA2495 /* HTTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = ""; }; + 35828B01291363170047D96D /* ImageCacheable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheable.swift; sourceTree = ""; }; + 35828B03291364420047D96D /* ImageCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheManager.swift; sourceTree = ""; }; + 35995AF02914E663007EDD1E /* URLManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLManager.swift; sourceTree = ""; }; 35B483BE290F59A60083012D /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; }; + 35BE6CC82911F95F00743A5C /* CustomCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCollectionView.swift; sourceTree = ""; }; + 35BE6CCA291207DE00743A5C /* CellSelctable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellSelctable.swift; sourceTree = ""; }; + 35BE6CCD29120B8600743A5C /* Extension+NSMutableAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+NSMutableAttributedString.swift"; sourceTree = ""; }; C70FB0F725BEF61C00C9924E /* OpenMarket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenMarket.app; sourceTree = BUILT_PRODUCTS_DIR; }; C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - C70FB0FE25BEF61C00C9924E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + C70FB0FE25BEF61C00C9924E /* ProductListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListViewController.swift; sourceTree = ""; }; C70FB10125BEF61C00C9924E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; C70FB10325BEF61D00C9924E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C70FB10625BEF61D00C9924E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C70FB10825BEF61D00C9924E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FB07033E29062A55006775BD /* ProductListPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListPage.swift; sourceTree = ""; }; FB07034029062E66006775BD /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; + FBC6E024291C841900119301 /* CellIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellIdentifier.swift; sourceTree = ""; }; + FBE9C293291345C4009477FA /* GridPageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridPageCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,27 +101,30 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 351049702907A4A100F92E73 /* Error */ = { + 3510498E2907C3B100F92E73 /* OpenMarketTests */ = { isa = PBXGroup; children = ( - 351049712907A4AC00F92E73 /* Error.swift */, + 351049992907C55F00F92E73 /* Mock.json */, + 3510498F2907C3B100F92E73 /* OpenMarketTests.swift */, ); - path = Error; + path = OpenMarketTests; sourceTree = ""; }; - 3510498E2907C3B100F92E73 /* OpenMarketTests */ = { + 351414B42910DF3B00ACD6EF /* Cell */ = { isa = PBXGroup; children = ( - 351049992907C55F00F92E73 /* Mock.json */, - 3510498F2907C3B100F92E73 /* OpenMarketTests.swift */, + 351414B52910DFAD00ACD6EF /* ListPageCell.swift */, + FBE9C293291345C4009477FA /* GridPageCell.swift */, + FBC6E024291C841900119301 /* CellIdentifier.swift */, ); - path = OpenMarketTests; + path = Cell; sourceTree = ""; }; 352505C2290616E6001A8700 /* Controller */ = { isa = PBXGroup; children = ( - C70FB0FE25BEF61C00C9924E /* ViewController.swift */, + 351414B42910DF3B00ACD6EF /* Cell */, + C70FB0FE25BEF61C00C9924E /* ProductListViewController.swift */, ); path = Controller; sourceTree = ""; @@ -116,6 +135,7 @@ C70FB10025BEF61C00C9924E /* Main.storyboard */, C70FB10325BEF61D00C9924E /* Assets.xcassets */, C70FB10525BEF61D00C9924E /* LaunchScreen.storyboard */, + 35BE6CC82911F95F00743A5C /* CustomCollectionView.swift */, ); path = View; sourceTree = ""; @@ -123,6 +143,7 @@ 352505C4290616F5001A8700 /* Model */ = { isa = PBXGroup; children = ( + 35828B002913630D0047D96D /* Cache */, 353A9D6B2908C10300FA2495 /* Network */, 353A9D6A2908C0F700FA2495 /* Type */, ); @@ -132,6 +153,8 @@ 353A9D672908BFE200FA2495 /* Protocol */ = { isa = PBXGroup; children = ( + 35BE6CCA291207DE00743A5C /* CellSelctable.swift */, + 35828B01291363170047D96D /* ImageCacheable.swift */, ); path = Protocol; sourceTree = ""; @@ -156,10 +179,27 @@ 353A9D712908CAD900FA2495 /* NetworkManager.swift */, 353A9D732908CB6600FA2495 /* NetworkError.swift */, 353A9D752908CD3500FA2495 /* HTTP.swift */, + 35995AF02914E663007EDD1E /* URLManager.swift */, ); path = Network; sourceTree = ""; }; + 35828B002913630D0047D96D /* Cache */ = { + isa = PBXGroup; + children = ( + 35828B03291364420047D96D /* ImageCacheManager.swift */, + ); + path = Cache; + sourceTree = ""; + }; + 35BE6CCC29120B6F00743A5C /* Extension */ = { + isa = PBXGroup; + children = ( + 35BE6CCD29120B8600743A5C /* Extension+NSMutableAttributedString.swift */, + ); + path = Extension; + sourceTree = ""; + }; C70FB0EE25BEF61C00C9924E = { isa = PBXGroup; children = ( @@ -181,10 +221,10 @@ C70FB0F925BEF61C00C9924E /* OpenMarket */ = { isa = PBXGroup; children = ( + 35BE6CCC29120B6F00743A5C /* Extension */, C70FB0FA25BEF61C00C9924E /* AppDelegate.swift */, C70FB0FC25BEF61C00C9924E /* SceneDelegate.swift */, 353A9D672908BFE200FA2495 /* Protocol */, - 351049702907A4A100F92E73 /* Error */, 352505C4290616F5001A8700 /* Model */, 352505C3290616F0001A8700 /* View */, 352505C2290616E6001A8700 /* Controller */, @@ -306,19 +346,27 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C70FB0FF25BEF61C00C9924E /* ViewController.swift in Sources */, + FBC6E025291C841900119301 /* CellIdentifier.swift in Sources */, + C70FB0FF25BEF61C00C9924E /* ProductListViewController.swift in Sources */, 351049762907B60A00F92E73 /* Image.swift in Sources */, FB07033F29062A55006775BD /* ProductListPage.swift in Sources */, C70FB0FB25BEF61C00C9924E /* AppDelegate.swift in Sources */, 353A9D722908CAD900FA2495 /* NetworkManager.swift in Sources */, + 35828B02291363170047D96D /* ImageCacheable.swift in Sources */, C70FB0FD25BEF61C00C9924E /* SceneDelegate.swift in Sources */, - 351049722907A4AC00F92E73 /* Error.swift in Sources */, + 35BE6CC92911F95F00743A5C /* CustomCollectionView.swift in Sources */, 353A9D702908C5E500FA2495 /* Currency.swift in Sources */, 353A9D762908CD3500FA2495 /* HTTP.swift in Sources */, FB07034129062E66006775BD /* Page.swift in Sources */, + 35828B04291364420047D96D /* ImageCacheManager.swift in Sources */, 353A9D742908CB6600FA2495 /* NetworkError.swift in Sources */, + 35BE6CCE29120B8600743A5C /* Extension+NSMutableAttributedString.swift in Sources */, + 351414B62910DFAD00ACD6EF /* ListPageCell.swift in Sources */, + FBE9C294291345C4009477FA /* GridPageCell.swift in Sources */, 353A9D6E2908C1FE00FA2495 /* Request.swift in Sources */, + 35995AF12914E663007EDD1E /* URLManager.swift in Sources */, 351049742907B56E00F92E73 /* Vendor.swift in Sources */, + 35BE6CCB291207DE00743A5C /* CellSelctable.swift in Sources */, 35B483BF290F59A60083012D /* Product.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -362,7 +410,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 45K89U54K6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.JungJaeGeun.OpenMarketTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -382,7 +430,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 45K89U54K6; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.JungJaeGeun.OpenMarketTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -516,7 +564,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = OpenMarket/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -535,7 +583,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = OpenMarket/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/OpenMarket/OpenMarket/Controller/Cell/CellIdentifier.swift b/OpenMarket/OpenMarket/Controller/Cell/CellIdentifier.swift new file mode 100644 index 0000000..1c6dfaf --- /dev/null +++ b/OpenMarket/OpenMarket/Controller/Cell/CellIdentifier.swift @@ -0,0 +1,13 @@ +// +// CellID.swift +// OpenMarket +// +// Created by 정연호 on 2022/11/10. +// + +import Foundation + +enum CellIdentifier { + static let listCellID: String = "ListPageCell" + static let gridCellID: String = "GridPageCell" +} diff --git a/OpenMarket/OpenMarket/Controller/Cell/GridPageCell.swift b/OpenMarket/OpenMarket/Controller/Cell/GridPageCell.swift new file mode 100644 index 0000000..2202af4 --- /dev/null +++ b/OpenMarket/OpenMarket/Controller/Cell/GridPageCell.swift @@ -0,0 +1,155 @@ +// +// GridPageCell.swift +// OpenMarket +// +// Created by 정연호 on 2022/11/03. +// + +import UIKit + +final class GridPageCell: UICollectionViewCell, CellSelectable { + var productId: Int? + + private lazy var thumbnailImage: UIImageView = UIImageView() + + private lazy var productTitle: UILabel = { + let lable = UILabel() + lable.font = UIFont.preferredFont(forTextStyle: .headline) + + return lable + }() + + private lazy var productPriceLabel: UILabel = { + let lable = UILabel() + lable.font = UIFont.preferredFont(forTextStyle: .callout) + lable.textColor = .gray + + return lable + }() + + private lazy var productDiscountLabel: UILabel = { + let lable = UILabel() + lable.font = UIFont.preferredFont(forTextStyle: .callout) + lable.textColor = .gray + + return lable + }() + + private lazy var productStockLabel: UILabel = { + let lable = UILabel() + lable.font = UIFont.preferredFont(forTextStyle: .callout) + + return lable + }() + + private lazy var priceLabelStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [productPriceLabel, productDiscountLabel]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.alignment = .center + stackView.distribution = .equalSpacing + stackView.axis = .vertical + stackView.spacing = 1.0 + + return stackView + }() + + private lazy var vStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [thumbnailImage, productTitle, priceLabelStackView, productStockLabel]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.alignment = .center + stackView.distribution = .equalSpacing + stackView.axis = .vertical + + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setCellLayer() + cellConstraint() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + productPriceLabel.attributedText = nil + thumbnailImage.image = nil + } + + private func setCellLayer() { + contentView.layer.borderColor = UIColor.gray.cgColor + contentView.layer.borderWidth = 1 + contentView.layer.cornerRadius = 10 + } + + private func cellConstraint() { + let inset: CGFloat = 20 + + contentView.addSubview(vStackView) + NSLayoutConstraint.activate([ + vStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset), + vStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset), + vStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + vStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + thumbnailImage.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.8), + thumbnailImage.heightAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.6) + ]) + } + + func configure(page: Page) { + productId = page.id + productTitle.text = page.name + setThumbnailImage(thumbnail: page.thumbnail) + setPriceLabel(currency: page.currency, price: page.price, discountedPrice: page.discountedPrice) + setStockLabel(stock: page.stock) + } + + private func setThumbnailImage(thumbnail: String) { + ImageCacheManager.loadImage(stringUrl: thumbnail) { image in + guard let thumbnailImage = image else { return } + DispatchQueue.main.async { [weak self] in + self?.thumbnailImage.image = thumbnailImage + } + } + } + + private func setPriceLabel(currency: Currency, price: Double, discountedPrice: Double) { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + switch currency { + case .krw: + numberFormatter.maximumFractionDigits = 0 + case .usd: + numberFormatter.maximumFractionDigits = 1 + } + + let priceString = numberFormatter.string(for: price) ?? "" + let discountedPriceString = numberFormatter.string(for: discountedPrice) ?? "" + + + + if discountedPrice == 0 { + productDiscountLabel.isHidden = true + productPriceLabel.text = "\(currency.rawValue) \(priceString)" + } else { + productDiscountLabel.isHidden = false + let priceText = "\(currency.rawValue) \(priceString)" + productPriceLabel.attributedText = NSMutableAttributedString(allText: priceText, previousText: priceText) + productDiscountLabel.text = "\(currency.rawValue) \(discountedPriceString)" + } + } + + private func setStockLabel(stock: Int) { + if stock == 0 { + productStockLabel.text = "품절" + productStockLabel.textColor = .orange + } else { + productStockLabel.text = "잔여수량 : \(stock)" + productStockLabel.textColor = .gray + } + } +} diff --git a/OpenMarket/OpenMarket/Controller/Cell/ListPageCell.swift b/OpenMarket/OpenMarket/Controller/Cell/ListPageCell.swift new file mode 100644 index 0000000..2859de9 --- /dev/null +++ b/OpenMarket/OpenMarket/Controller/Cell/ListPageCell.swift @@ -0,0 +1,131 @@ +// +// PageCell.swift +// OpenMarket +// +// Created by 정재근 on 2022/11/01. +// + +import UIKit + +final class ListPageCell: UICollectionViewListCell, CellSelectable { + var productId: Int? + private let imageLength: CGFloat = 80 + private var contentLayout: [NSLayoutConstraint]? + private lazy var listContentView = UIListContentView(configuration: .subtitleCell()) + private lazy var thumbnailImage = UIImageView() + private let stockLabel: UILabel = { + let label = UILabel() + label.textAlignment = .right + label.numberOfLines = 2 + label.font = .systemFont(ofSize: 15) + + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + constraint() + } + + override func prepareForReuse() { + super.prepareForReuse() + thumbnailImage.image = nil + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func listConfiguration() -> UIListContentConfiguration { + .subtitleCell() + } + + private func constraint() { + guard contentLayout == nil else { return } + let indicator = UICellAccessory.disclosureIndicator() + self.accessories = [indicator] + + [listContentView, stockLabel, thumbnailImage].forEach { + contentView.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + let height = contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: imageLength) + height.priority = UILayoutPriority(999) + + let layouts = [ + height, + thumbnailImage.widthAnchor.constraint(equalToConstant: imageLength-5), + thumbnailImage.heightAnchor.constraint(equalToConstant: imageLength-5), + thumbnailImage.trailingAnchor.constraint(equalTo: listContentView.leadingAnchor), + thumbnailImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + thumbnailImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + listContentView.topAnchor.constraint(equalTo: contentView.topAnchor), + listContentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + + stockLabel.leadingAnchor.constraint(equalTo: listContentView.trailingAnchor), + stockLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5), + stockLabel.widthAnchor.constraint(equalTo: listContentView.widthAnchor, multiplier: 0.5), + stockLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15) + ] + NSLayoutConstraint.activate(layouts) + } + + func configureCell(page: Page) { + self.productId = page.id + setListContentView(name: page.name, currency: page.currency, price: page.price, discountedPrice: page.discountedPrice) + setThumbnailImage(thumbnail: page.thumbnail) + setStockLabel(stock: page.stock) + } + + private func setListContentView(name: String, currency: Currency, price: Double, discountedPrice: Double) { + var configure = self.listConfiguration() + + configure.text = name + configure.textProperties.font = .preferredFont(forTextStyle: .headline) + configure.secondaryTextProperties.font = .preferredFont(forTextStyle: .callout) + configure.secondaryTextProperties.color = .gray + + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + switch currency { + case .usd: + numberFormatter.maximumFractionDigits = 1 + case .krw: + numberFormatter.maximumFractionDigits = 0 + } + + let priceString = numberFormatter.string(for: price) ?? "" + + if discountedPrice == 0 { + configure.secondaryText = "\(currency.rawValue) \(priceString)" + } else { + let discountPrice = numberFormatter.string(for: discountedPrice) ?? "" + let priviousPrice = "\(currency.rawValue) \(priceString)" + let text = priviousPrice + " \(currency.rawValue) \(discountPrice)" + configure.secondaryAttributedText = NSMutableAttributedString(allText: text, previousText: priviousPrice) + } + listContentView.configuration = configure + } + + private func setThumbnailImage(thumbnail: String) { + ImageCacheManager.loadImage(stringUrl: thumbnail) { image in + guard let thubnailImage = image else { return } + DispatchQueue.main.async { [weak self] in + self?.thumbnailImage.image = thubnailImage + } + } + } + + private func setStockLabel(stock: Int) { + if stock == 0 { + stockLabel.text = "품절" + stockLabel.textColor = .orange + } else { + stockLabel.text = "잔여수량 : \(stock)" + stockLabel.textColor = .gray + } + } +} diff --git a/OpenMarket/OpenMarket/Controller/ProductListViewController.swift b/OpenMarket/OpenMarket/Controller/ProductListViewController.swift new file mode 100644 index 0000000..921f607 --- /dev/null +++ b/OpenMarket/OpenMarket/Controller/ProductListViewController.swift @@ -0,0 +1,158 @@ +// +// OpenMarket - ViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +final class ProductListViewController: UIViewController, UIGestureRecognizerDelegate { + + enum Section { + case main + } + // MARK: - const var + private var productListPage: ProductListPage? + private var dataSource: UICollectionViewDiffableDataSource! + // MARK: - UI Component + @IBOutlet weak private var viewTypeSegmentControl: UISegmentedControl! + @IBOutlet weak private var moveToAddViewButton: UIBarButtonItem! + @IBOutlet weak private var pageCollectionView: CustomCollectionView! + private let swipeGesture = UISwipeGestureRecognizer() + + private lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView() + activityIndicator.frame = CGRect(x: 0, y: 0, width: 50, height: 50) + activityIndicator.center = self.view.center + activityIndicator.color = .blue + activityIndicator.hidesWhenStopped = true + activityIndicator.style = UIActivityIndicatorView.Style.medium + activityIndicator.stopAnimating() + + return activityIndicator + }() + // MARK: - LifeCycle + override func viewDidLoad() { + super.viewDidLoad() + configure() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.pageCollectionView.reloadData() + } + // MARK: - configure + private func configure() { + configureDataSource() + pageCollectionView.changeLayout(type: .list) + registerCell() + addIndicator() + getProductListPage() + addSwipeGesture() + } + + private func addIndicator() { + self.view.addSubview(activityIndicator) + self.activityIndicator.startAnimating() + } + + private func registerCell() { + pageCollectionView.register(ListPageCell.self, forCellWithReuseIdentifier: CellIdentifier.listCellID) + pageCollectionView.register(GridPageCell.self, forCellWithReuseIdentifier: CellIdentifier.gridCellID) + } + + private func addSwipeGesture() { + swipeGesture.direction = .left + swipeGesture.addTarget(self, action: #selector(didSwipe)) + self.view.addGestureRecognizer(swipeGesture) + } + // MARK: - Data + private func getProductListPage() { + DispatchQueue.global().async { + NetworkManager.getData(requestType: .productList(pageNo: 1, itemsPerPage: 100)) { result in + switch result { + case .success(let data): + do { + let productListPage = try JSONDecoder().decode(ProductListPage.self, from: data) + DispatchQueue.main.async { [weak self] in + self?.setSnapShot(pages: productListPage.pages) + self?.pageCollectionView.reloadData() + } + } catch { + print(error.localizedDescription) + } + case .failure(let error): + print(error.localizedDescription) + } + } + } + } + + private func setSnapShot(pages: [Page]) { + var snapShot = NSDiffableDataSourceSnapshot() + snapShot.appendSections([.main]) + DispatchQueue.main.async { [weak self] in + snapShot.appendItems(pages) + self?.dataSource.apply(snapShot, animatingDifferences: false) + self?.activityIndicator.stopAnimating() + } + } + + private func configureDataSource() { + self.dataSource = UICollectionViewDiffableDataSource(collectionView: pageCollectionView) { + pageCollectionView, indexPath, itemIdentifier in + + switch CustomCollectionView.ViewType(rawValue: self.viewTypeSegmentControl.selectedSegmentIndex) { + case .list: + guard let cell = pageCollectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier.listCellID, for: indexPath) as? ListPageCell else { + + return UICollectionViewListCell() + } + cell.configureCell(page: itemIdentifier) + + return cell + case .grid: + guard let cell = pageCollectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier.gridCellID, for: indexPath) as? GridPageCell else { + + return UICollectionViewCell() + } + cell.configure(page: itemIdentifier) + + return cell + case .none: + return UICollectionViewListCell() + } + } + } + // MARK: - Action + private func changeLayoutAction() { + pageCollectionView.reloadData() + activityIndicator.startAnimating() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [self] in + activityIndicator.stopAnimating() + } + } + + @IBAction func didChangeSegmentControl(_ sender: UISegmentedControl) { + switch CustomCollectionView.ViewType(rawValue: sender.selectedSegmentIndex) { + case .list: + pageCollectionView.changeLayout(type: .list) + changeLayoutAction() + case .grid: + pageCollectionView.changeLayout(type: .grid) + changeLayoutAction() + case .none: + return + } + } + + @objc private func didSwipe(sender: UIGestureRecognizer) { + let isLeftSwipe = viewTypeSegmentControl.selectedSegmentIndex == 0 + swipeGesture.direction = isLeftSwipe ? .right : .left + + viewTypeSegmentControl.selectedSegmentIndex = isLeftSwipe ? 1 : 0 + + pageCollectionView.changeLayout(type: isLeftSwipe ? .grid : .list) + pageCollectionView.reloadData() + } +} diff --git a/OpenMarket/OpenMarket/Controller/ViewController.swift b/OpenMarket/OpenMarket/Controller/ViewController.swift deleted file mode 100644 index 6d5b81c..0000000 --- a/OpenMarket/OpenMarket/Controller/ViewController.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// OpenMarket - ViewController.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - } -} diff --git a/OpenMarket/OpenMarket/Error/Error.swift b/OpenMarket/OpenMarket/Error/Error.swift deleted file mode 100644 index 32704dc..0000000 --- a/OpenMarket/OpenMarket/Error/Error.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Error.swift -// OpenMarket -// -// Created by 정재근 on 2022/10/25. -// - -import Foundation - - diff --git a/OpenMarket/OpenMarket/Extension/Extension+NSMutableAttributedString.swift b/OpenMarket/OpenMarket/Extension/Extension+NSMutableAttributedString.swift new file mode 100644 index 0000000..0a592c7 --- /dev/null +++ b/OpenMarket/OpenMarket/Extension/Extension+NSMutableAttributedString.swift @@ -0,0 +1,17 @@ +// +// Extension+NSMutableAttributedString.swift +// OpenMarket +// +// Created by 정재근 on 2022/11/02. +// + +import UIKit + +extension NSMutableAttributedString { + convenience init(allText: String, previousText: String) { + self.init(string: allText) + self.addAttribute(.foregroundColor, value: UIColor.red, range: (previousText as NSString).range(of: previousText)) + self.addAttribute(.strikethroughColor, value: UIColor.red, range: (previousText as NSString).range(of: previousText)) + self.addAttribute(.strikethroughStyle, value: 1, range: (previousText as NSString).range(of: previousText)) + } +} diff --git a/OpenMarket/OpenMarket/Model/Cache/ImageCacheManager.swift b/OpenMarket/OpenMarket/Model/Cache/ImageCacheManager.swift new file mode 100644 index 0000000..140b027 --- /dev/null +++ b/OpenMarket/OpenMarket/Model/Cache/ImageCacheManager.swift @@ -0,0 +1,32 @@ +// +// ImageCache.swift +// OpenMarket +// +// Created by 정재근 on 2022/11/03. +// + +import UIKit + +final class ImageCacheManager: ImageCacheable { + static var cache = NSCache() + + static func loadImage(stringUrl: String, completion: @escaping (UIImage?) -> Void) { + if let image = ImageCacheManager.cache.object(forKey: stringUrl as NSString) { + completion(image) + } + + NetworkManager.getData(requestType: .thubnailImage(thumnailURL: stringUrl)) { result in + switch result { + case .success(let data): + guard let image = UIImage(data: data) else { + completion(nil) + return + } + ImageCacheManager.cache.setObject(image, forKey: stringUrl as NSString) + completion(image) + case .failure(let error): + print(error.localizedDescription) + } + } + } +} diff --git a/OpenMarket/OpenMarket/Model/Network/NetworkManager.swift b/OpenMarket/OpenMarket/Model/Network/NetworkManager.swift index 2a09c71..0b4a02d 100644 --- a/OpenMarket/OpenMarket/Model/Network/NetworkManager.swift +++ b/OpenMarket/OpenMarket/Model/Network/NetworkManager.swift @@ -8,14 +8,11 @@ import Foundation final class NetworkManager { - let urlSession: URLSession + static let urlSession: URLSession = URLSession.shared + static var urlManager: URLManager = URLManager() - init() { - self.urlSession = URLSession.shared - } - - func dataTask(request: URLRequest, completion: @escaping(Result) -> Void ) { - let task = self.urlSession.dataTask(with: request) { (data, urlResponse, error) in + static func dataTask(request: URLRequest, completion: @escaping(Result) -> Void ) { + let task = NetworkManager.urlSession.dataTask(with: request) { (data, urlResponse, error) in guard let httpResponse = urlResponse as? HTTPURLResponse, (HTTPStatus.ok.range).contains(httpResponse.statusCode) else { return completion(.failure(.statusCodeError)) @@ -30,13 +27,13 @@ final class NetworkManager { task.resume() } - func getData(requestType: Request, completion: @escaping(Result) -> Void ) { - guard let url = URL(string: requestType.url) else { + static func getData(requestType: Request, completion: @escaping(Result) -> Void ) { + guard let url = urlManager.requestURL(requestType: requestType)?.url else { return completion(.failure(NetworkError.invalidURL)) } var request = URLRequest(url: url) request.httpMethod = HTTPMethod.get - dataTask(request: request, completion: completion) + NetworkManager.dataTask(request: request, completion: completion) } } diff --git a/OpenMarket/OpenMarket/Model/Network/Request.swift b/OpenMarket/OpenMarket/Model/Network/Request.swift index 17580ec..db9e475 100644 --- a/OpenMarket/OpenMarket/Model/Network/Request.swift +++ b/OpenMarket/OpenMarket/Model/Network/Request.swift @@ -5,20 +5,10 @@ // Created by 정재근 on 2022/10/26. // -import Foundation +import UIKit enum Request { - static let apiHost = "https://openmarket.yagom-academy.kr" - case productList(pageNo: Int, itemsPerPage: Int) case produntInfo(productId: Int) - - var url: String { - switch self { - case .productList(pageNo: let pageNo, itemsPerPage: let itemsPerPage): - return "\(Request.apiHost)/api/products?page_no=\(pageNo)&items_per_page=\(itemsPerPage)" - case .produntInfo(productId: let productId): - return "\(Request.apiHost)/api/products/\(productId)" - } - } + case thubnailImage(thumnailURL: String) } diff --git a/OpenMarket/OpenMarket/Model/Network/URLManager.swift b/OpenMarket/OpenMarket/Model/Network/URLManager.swift new file mode 100644 index 0000000..67b1b5d --- /dev/null +++ b/OpenMarket/OpenMarket/Model/Network/URLManager.swift @@ -0,0 +1,45 @@ +// +// URL.swift +// OpenMarket +// +// Created by 정재근 on 2022/11/04. +// + +import UIKit + +struct URLManager { + let scheme = "https" + let host = "openmarket.yagom-academy.kr" + let path = "/api/products" + var baseComponent: URLComponents + + init() { + var urlComponents = URLComponents() + urlComponents.scheme = scheme + urlComponents.host = host + self.baseComponent = urlComponents + } + + mutating func requestURL(requestType: Request) -> URLComponents? { + switch requestType { + case .productList(pageNo: let pageNo, itemsPerPage: let itemsPerPage): + var component = self.baseComponent + component.path = self.path + component.queryItems = [URLQueryItem(name: "page_no", value: "\(pageNo)"), + URLQueryItem(name: "items_per_page", value: "\(itemsPerPage)")] + + return component + case .produntInfo(productId: let productId): + var component = self.baseComponent + component.path = self.path + "/\(productId)" + + return component + case .thubnailImage(thumnailURL: let thumbnailURL): + guard let component = URLComponents(string: thumbnailURL) else { + return nil + } + + return component + } + } +} diff --git a/OpenMarket/OpenMarket/Model/Type/Page.swift b/OpenMarket/OpenMarket/Model/Type/Page.swift index aac5b6f..d92fdc4 100644 --- a/OpenMarket/OpenMarket/Model/Type/Page.swift +++ b/OpenMarket/OpenMarket/Model/Type/Page.swift @@ -7,16 +7,17 @@ import Foundation -struct Page: Decodable { +struct Page: Decodable, Hashable { let id: Int let vendorID: Int + let vendorName: String let name: String let description: String let thumbnail: String let currency: Currency - let price: Int - let bargainPrice: Int - let discountedPrice: Int + let price: Double + let bargainPrice: Double + let discountedPrice: Double let stock: Int let createdAt: String let issuedAt: String @@ -24,6 +25,7 @@ struct Page: Decodable { enum CodingKeys: String, CodingKey { case id case vendorID = "vendor_id" + case vendorName case name case description case thumbnail diff --git a/OpenMarket/OpenMarket/Model/Type/Product.swift b/OpenMarket/OpenMarket/Model/Type/Product.swift index 6baf244..7aee1da 100644 --- a/OpenMarket/OpenMarket/Model/Type/Product.swift +++ b/OpenMarket/OpenMarket/Model/Type/Product.swift @@ -14,9 +14,9 @@ struct Product: Decodable { let description: String let thumbnail: String let currency: Currency - let price: Int - let bargainPrice: Int - let discountedPrice: Int + let price: Double + let bargainPrice: Double + let discountedPrice: Double let stock: Int let images: [Image] let vendors: Vendor diff --git a/OpenMarket/OpenMarket/Protocol/CellSelctable.swift b/OpenMarket/OpenMarket/Protocol/CellSelctable.swift new file mode 100644 index 0000000..1976e20 --- /dev/null +++ b/OpenMarket/OpenMarket/Protocol/CellSelctable.swift @@ -0,0 +1,12 @@ +// +// CellSelctable.swift +// OpenMarket +// +// Created by 정재근 on 2022/11/02. +// + +import Foundation + +protocol CellSelectable { + var productId: Int? { get } +} diff --git a/OpenMarket/OpenMarket/Protocol/ImageCacheable.swift b/OpenMarket/OpenMarket/Protocol/ImageCacheable.swift new file mode 100644 index 0000000..81b53fa --- /dev/null +++ b/OpenMarket/OpenMarket/Protocol/ImageCacheable.swift @@ -0,0 +1,12 @@ +// +// File.swift +// OpenMarket +// +// Created by 정재근 on 2022/11/03. +// + +import UIKit + +protocol ImageCacheable { + static func loadImage(stringUrl: String, completion: @escaping (UIImage?) -> Void) +} diff --git a/OpenMarket/OpenMarket/View/Base.lproj/Main.storyboard b/OpenMarket/OpenMarket/View/Base.lproj/Main.storyboard index 25a7638..70e733e 100644 --- a/OpenMarket/OpenMarket/View/Base.lproj/Main.storyboard +++ b/OpenMarket/OpenMarket/View/Base.lproj/Main.storyboard @@ -1,24 +1,125 @@ - + - + + + + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenMarket/OpenMarket/View/CustomCollectionView.swift b/OpenMarket/OpenMarket/View/CustomCollectionView.swift new file mode 100644 index 0000000..a27adbe --- /dev/null +++ b/OpenMarket/OpenMarket/View/CustomCollectionView.swift @@ -0,0 +1,51 @@ +// +// CustomCollectionView.swift +// OpenMarket +// +// Created by 정재근 on 2022/11/02. +// + +import UIKit + +final class CustomCollectionView: UICollectionView { + + enum ViewType: Int { + case list = 0 + case grid = 1 + } + + private lazy var listLayout: UICollectionViewCompositionalLayout = { + let configuration = UICollectionLayoutListConfiguration(appearance: .plain) + let layout = UICollectionViewCompositionalLayout.list(using: configuration) + + return layout + }() + + private lazy var gridLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + let inset: CGFloat = 10 + let rowItem = 2 + let width = (safeAreaLayoutGuide.layoutFrame.width / CGFloat(rowItem)) - (inset * 1.5) + let height = (safeAreaLayoutGuide.layoutFrame.height / 2.6) - inset + + layout.minimumLineSpacing = inset + layout.minimumInteritemSpacing = inset + layout.scrollDirection = .vertical + layout.itemSize = CGSize(width: width, height: height) + layout.sectionInset.left = inset + layout.sectionInset.right = inset + + return layout + }() + + func changeLayout(type: ViewType) { + switch type { + case .list: + setCollectionViewLayout(listLayout, animated: true) + case.grid: + setCollectionViewLayout(gridLayout, animated: true) + } + + self.setContentOffset(CGPoint(x: 0, y: 0), animated: false) + } +} diff --git a/OpenMarket/OpenMarketTests/Mock.json b/OpenMarket/OpenMarketTests/Mock.json index 60955cb..0600f6e 100644 --- a/OpenMarket/OpenMarketTests/Mock.json +++ b/OpenMarket/OpenMarketTests/Mock.json @@ -4,13 +4,14 @@ "totalCount": 10, "offset": 0, "limit": 20, - "lastPage": 1, - "hasNext": false, - "hasPrev": false, + "last_page": 1, + "has_next": false, + "has_prev": false, "pages": [ { "id": 20, "vendor_id": 3, + "vendorName": "test", "name": "Test Product", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/3/thumb/5a0cd56b6d3411ecabfa97fd953cf965.jpg", "currency": "KRW", @@ -24,6 +25,7 @@ { "id": 19, "vendor_id": 3, + "vendorName": "test", "name": "Test Product", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/3/thumb/5493c5886d3411ecabfa433b72466e30.jpg", "currency": "KRW", @@ -37,6 +39,7 @@ { "id": 18, "vendor_id": 3, + "vendorName": "test", "name": "Test Product", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/3/thumb/57501bd56d3311ecabfa3fd049db386d.jpeg", "currency": "KRW", @@ -50,6 +53,7 @@ { "id": 17, "vendor_id": 3, + "vendorName": "test", "name": "Test Product", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/3/thumb/1d02e0726d3311ecabfafdc8ea5e6b5b.jpg", "currency": "KRW", @@ -63,6 +67,7 @@ { "id": 16, "vendor_id": 2, + "vendorName": "test", "name": "팥빙수", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/2/thumb/8279f1c16c6111ecbf33d51698ab7ead.png", "currency": "KRW", @@ -76,6 +81,7 @@ { "id": 15, "vendor_id": 3, + "vendorName": "test", "name": "pizza", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/3/thumb/f510cb6e689f11ecbf33d124b2c61dc4.jpg", "currency": "KRW", @@ -89,6 +95,7 @@ { "id": 13, "vendor_id": 2, + "vendorName": "test", "name": "팥빙수", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/2/thumb/f70ad56a689911ecbf33f11af721febf.png", "currency": "KRW", @@ -102,6 +109,7 @@ { "id": 4, "vendor_id": 3, + "vendorName": "test", "name": "Test Product", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/3/thumb/87aa7c8966df11ecad1df993f20d4a2a.jpg", "currency": "KRW", @@ -115,6 +123,7 @@ { "id": 3, "vendor_id": 3, + "vendorName": "test", "name": "Test Product", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/3/thumb/2f6021a066dc11ec9626955448777bf5.jpg", "currency": "KRW", @@ -128,6 +137,7 @@ { "id": 2, "vendor_id": 2, + "vendorName": "test", "name": "팥빙수", "thumbnail": "https://s3.ap-northeast-2.amazonaws.com/media.yagom-academy.kr/training-resources/2/thumb/a3257844661911ec8eff5b6e36134cb4.png", "currency": "KRW", @@ -138,8 +148,5 @@ "created_at": "2021-12-26T00:00:00.00", "issued_at": "2021-12-26T00:00:00.00" } - ], - "last_page": 1, - "has_next": false, - "has_prev": false + ] }