diff --git a/.gitignore b/.gitignore index 145fc4a6..cda73351 100644 --- a/.gitignore +++ b/.gitignore @@ -100,4 +100,7 @@ iOSInjectionProject/ /*.gcno **/xcshareddata/WorkspaceSettings.xcsettings +### API KEY ### +BoxOffice/API_KEY.plist + # End of https://www.toptal.com/developers/gitignore/api/xcode,swift \ No newline at end of file diff --git a/BoxOffice.xcodeproj/project.pbxproj b/BoxOffice.xcodeproj/project.pbxproj index b359717d..637204a6 100644 --- a/BoxOffice.xcodeproj/project.pbxproj +++ b/BoxOffice.xcodeproj/project.pbxproj @@ -7,14 +7,32 @@ objects = { /* Begin PBXBuildFile section */ + 0A5096F02A8C902500A9AB75 /* RankIntensity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5096EF2A8C902500A9AB75 /* RankIntensity.swift */; }; + 0A9680FE2A7393DD00B78C61 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9680FD2A7393DD00B78C61 /* NetworkManager.swift */; }; + 0A9681012A73954C00B78C61 /* URLRequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9681002A73954C00B78C61 /* URLRequestError.swift */; }; + 0A9681052A7899A300B78C61 /* EndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9681042A7899A300B78C61 /* EndPoint.swift */; }; + 0A9681562A7CE0C900B78C61 /* NetworkManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9681552A7CE0C900B78C61 /* NetworkManagerError.swift */; }; + 0A96815A2A82239300B78C61 /* CustomListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9681592A82239300B78C61 /* CustomListCell.swift */; }; + 0A96815C2A84EE7700B78C61 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A96815B2A84EE7700B78C61 /* Item.swift */; }; + 0A96815E2A86086000B78C61 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A96815D2A86086000B78C61 /* DateFormatter+.swift */; }; 63DF20EF2970E1A0005DF7D1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20EE2970E1A0005DF7D1 /* AppDelegate.swift */; }; 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20F02970E1A0005DF7D1 /* SceneDelegate.swift */; }; 63DF20F32970E1A0005DF7D1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20F22970E1A0005DF7D1 /* ViewController.swift */; }; 63DF20F62970E1A0005DF7D1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F42970E1A0005DF7D1 /* Main.storyboard */; }; 63DF20F82970E1A1005DF7D1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */; }; 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */; }; + BA2AFE0F2A789593006C852C /* NetworkConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2AFE0E2A789593006C852C /* NetworkConfigurable.swift */; }; + BA2AFE112A78BE27006C852C /* NetworkConfigurableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2AFE102A78BE27006C852C /* NetworkConfigurableError.swift */; }; + BA3D99A92A78CD8E00D7737C /* API_KEY.plist in Resources */ = {isa = PBXBuildFile; fileRef = BA3D99A82A78CD8E00D7737C /* API_KEY.plist */; }; + BA3D99AC2A78D25C00D7737C /* Bundle+.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3D99AB2A78D25C00D7737C /* Bundle+.swift */; }; + BA527B142A87674400380A49 /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA527B132A87674400380A49 /* String+.swift */; }; + BA6717802A821EAE0036CB23 /* ItemListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA67177F2A821EAE0036CB23 /* ItemListCell.swift */; }; BAA6E4962A6F5724000569E2 /* BoxOffice.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA6E4952A6F5724000569E2 /* BoxOffice.swift */; }; BAA6E4AF2A6F8F6B000569E2 /* BoxOfficeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA6E4AE2A6F8F6B000569E2 /* BoxOfficeTests.swift */; }; + BAB7E7A62A8864C60023C4A6 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB7E7A52A8864C60023C4A6 /* DetailViewController.swift */; }; + BAF0908A2A7F2E550091C1B7 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF090892A7F2E550091C1B7 /* Section.swift */; }; + BAF0908C2A7F305B0091C1B7 /* UIConfigurationStateCustomKey+.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF0908B2A7F305B0091C1B7 /* UIConfigurationStateCustomKey+.swift */; }; + BAF0908E2A7F325F0091C1B7 /* UICellConfigurationState+.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF0908D2A7F325F0091C1B7 /* UICellConfigurationState+.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -28,6 +46,14 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0A5096EF2A8C902500A9AB75 /* RankIntensity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankIntensity.swift; sourceTree = ""; }; + 0A9680FD2A7393DD00B78C61 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + 0A9681002A73954C00B78C61 /* URLRequestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestError.swift; sourceTree = ""; }; + 0A9681042A7899A300B78C61 /* EndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPoint.swift; sourceTree = ""; }; + 0A9681552A7CE0C900B78C61 /* NetworkManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerError.swift; sourceTree = ""; }; + 0A9681592A82239300B78C61 /* CustomListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListCell.swift; sourceTree = ""; }; + 0A96815B2A84EE7700B78C61 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + 0A96815D2A86086000B78C61 /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; 63DF20EB2970E1A0005DF7D1 /* BoxOffice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BoxOffice.app; sourceTree = BUILT_PRODUCTS_DIR; }; 63DF20EE2970E1A0005DF7D1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 63DF20F02970E1A0005DF7D1 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -36,10 +62,20 @@ 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 63DF20FA2970E1A1005DF7D1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 63DF20FC2970E1A1005DF7D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BA2AFE0E2A789593006C852C /* NetworkConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfigurable.swift; sourceTree = ""; }; + BA2AFE102A78BE27006C852C /* NetworkConfigurableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfigurableError.swift; sourceTree = ""; }; + BA3D99A82A78CD8E00D7737C /* API_KEY.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = API_KEY.plist; sourceTree = ""; }; + BA3D99AB2A78D25C00D7737C /* Bundle+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+.swift"; sourceTree = ""; }; + BA527B132A87674400380A49 /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; + BA67177F2A821EAE0036CB23 /* ItemListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListCell.swift; sourceTree = ""; }; BAA6E4952A6F5724000569E2 /* BoxOffice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOffice.swift; sourceTree = ""; }; BAA6E4AC2A6F8F6B000569E2 /* BoxOfficeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BoxOfficeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; BAA6E4AE2A6F8F6B000569E2 /* BoxOfficeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeTests.swift; sourceTree = ""; }; BAA6E4B52A6F8F79000569E2 /* BoxOfficeTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = BoxOfficeTestPlan.xctestplan; sourceTree = ""; }; + BAB7E7A52A8864C60023C4A6 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; + BAF090892A7F2E550091C1B7 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; + BAF0908B2A7F305B0091C1B7 /* UIConfigurationStateCustomKey+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIConfigurationStateCustomKey+.swift"; sourceTree = ""; }; + BAF0908D2A7F325F0091C1B7 /* UICellConfigurationState+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICellConfigurationState+.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -65,6 +101,8 @@ children = ( 63DF20F42970E1A0005DF7D1 /* Main.storyboard */, 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */, + BA67177F2A821EAE0036CB23 /* ItemListCell.swift */, + 0A9681592A82239300B78C61 /* CustomListCell.swift */, ); path = View; sourceTree = ""; @@ -73,6 +111,7 @@ isa = PBXGroup; children = ( 63DF20F22970E1A0005DF7D1 /* ViewController.swift */, + BAB7E7A52A8864C60023C4A6 /* DetailViewController.swift */, ); path = Controller; sourceTree = ""; @@ -87,6 +126,16 @@ path = Resource; sourceTree = ""; }; + 0A9680FF2A73950A00B78C61 /* Error */ = { + isa = PBXGroup; + children = ( + 0A9681002A73954C00B78C61 /* URLRequestError.swift */, + BA2AFE102A78BE27006C852C /* NetworkConfigurableError.swift */, + 0A9681552A7CE0C900B78C61 /* NetworkManagerError.swift */, + ); + path = Error; + sourceTree = ""; + }; 63DF20E22970E1A0005DF7D1 = { isa = PBXGroup; children = ( @@ -108,19 +157,40 @@ 63DF20ED2970E1A0005DF7D1 /* BoxOffice */ = { isa = PBXGroup; children = ( + BA3D99AA2A78D22900D7737C /* Extension */, + 0A9680FF2A73950A00B78C61 /* Error */, 0A9680FA2A6FA44C00B78C61 /* Resource */, 0A9680F92A6FA3C800B78C61 /* Controller */, 0A9680F82A6FA3BE00B78C61 /* View */, BAA6E4942A6F56D2000569E2 /* Model */, 63DF20FC2970E1A1005DF7D1 /* Info.plist */, + BA3D99A82A78CD8E00D7737C /* API_KEY.plist */, ); path = BoxOffice; sourceTree = ""; }; + BA3D99AA2A78D22900D7737C /* Extension */ = { + isa = PBXGroup; + children = ( + BA3D99AB2A78D25C00D7737C /* Bundle+.swift */, + BAF0908B2A7F305B0091C1B7 /* UIConfigurationStateCustomKey+.swift */, + BAF0908D2A7F325F0091C1B7 /* UICellConfigurationState+.swift */, + 0A96815D2A86086000B78C61 /* DateFormatter+.swift */, + BA527B132A87674400380A49 /* String+.swift */, + ); + path = Extension; + sourceTree = ""; + }; BAA6E4942A6F56D2000569E2 /* Model */ = { isa = PBXGroup; children = ( BAA6E4952A6F5724000569E2 /* BoxOffice.swift */, + 0A9680FD2A7393DD00B78C61 /* NetworkManager.swift */, + BA2AFE0E2A789593006C852C /* NetworkConfigurable.swift */, + 0A9681042A7899A300B78C61 /* EndPoint.swift */, + BAF090892A7F2E550091C1B7 /* Section.swift */, + 0A96815B2A84EE7700B78C61 /* Item.swift */, + 0A5096EF2A8C902500A9AB75 /* RankIntensity.swift */, ); path = Model; sourceTree = ""; @@ -216,6 +286,7 @@ buildActionMask = 2147483647; files = ( 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */, + BA3D99A92A78CD8E00D7737C /* API_KEY.plist in Resources */, 63DF20F82970E1A1005DF7D1 /* Assets.xcassets in Resources */, 63DF20F62970E1A0005DF7D1 /* Main.storyboard in Resources */, ); @@ -235,9 +306,26 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0A5096F02A8C902500A9AB75 /* RankIntensity.swift in Sources */, + BA2AFE112A78BE27006C852C /* NetworkConfigurableError.swift in Sources */, + BA3D99AC2A78D25C00D7737C /* Bundle+.swift in Sources */, + 0A96815C2A84EE7700B78C61 /* Item.swift in Sources */, + BAF0908E2A7F325F0091C1B7 /* UICellConfigurationState+.swift in Sources */, + BA2AFE0F2A789593006C852C /* NetworkConfigurable.swift in Sources */, + BA527B142A87674400380A49 /* String+.swift in Sources */, + 0A9681012A73954C00B78C61 /* URLRequestError.swift in Sources */, + BAB7E7A62A8864C60023C4A6 /* DetailViewController.swift in Sources */, + 0A9681052A7899A300B78C61 /* EndPoint.swift in Sources */, + 0A96815A2A82239300B78C61 /* CustomListCell.swift in Sources */, 63DF20F32970E1A0005DF7D1 /* ViewController.swift in Sources */, + BAF0908C2A7F305B0091C1B7 /* UIConfigurationStateCustomKey+.swift in Sources */, BAA6E4962A6F5724000569E2 /* BoxOffice.swift in Sources */, + 0A9680FE2A7393DD00B78C61 /* NetworkManager.swift in Sources */, + 0A96815E2A86086000B78C61 /* DateFormatter+.swift in Sources */, 63DF20EF2970E1A0005DF7D1 /* AppDelegate.swift in Sources */, + BA6717802A821EAE0036CB23 /* ItemListCell.swift in Sources */, + BAF0908A2A7F2E550091C1B7 /* Section.swift in Sources */, + 0A9681562A7CE0C900B78C61 /* NetworkManagerError.swift in Sources */, 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/BoxOffice/API_KEY.plist b/BoxOffice/API_KEY.plist new file mode 100644 index 00000000..03182c79 --- /dev/null +++ b/BoxOffice/API_KEY.plist @@ -0,0 +1,12 @@ + + + + + + diff --git a/BoxOffice/Controller/DetailViewController.swift b/BoxOffice/Controller/DetailViewController.swift new file mode 100644 index 00000000..a668b2f7 --- /dev/null +++ b/BoxOffice/Controller/DetailViewController.swift @@ -0,0 +1,17 @@ +// +// DetailViewController.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/13. +// + +import UIKit + +class DetailViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .systemPink + } + +} diff --git a/BoxOffice/Controller/ViewController.swift b/BoxOffice/Controller/ViewController.swift index 208e0ecf..e179c75e 100644 --- a/BoxOffice/Controller/ViewController.swift +++ b/BoxOffice/Controller/ViewController.swift @@ -8,12 +8,163 @@ import UIKit class ViewController: UIViewController { - + var dateCountUpForTest: Int = 20230720 + let networkManager: NetworkManager = NetworkManager() + let refreshControl: UIRefreshControl = UIRefreshControl() + var dataSource: UICollectionViewDiffableDataSource? = nil + override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + + collectionView.delegate = self + + self.fetchDate() + self.configureHierarchy() + self.initRefreshControl() + + fetchBoxOfficeData { + DispatchQueue.main.async { + self.configureDataSource() + } + } + } + + func createLayout() -> UICollectionViewLayout { + let config = UICollectionLayoutListConfiguration(appearance: .plain) + return UICollectionViewCompositionalLayout.list(using: config) + } + + lazy var collectionView: UICollectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) + + func configureHierarchy() { + collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) } + + func configureDataSource() { + let cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in + cell.updateWithItem(item) + cell.accessories = [.disclosureIndicator()] + } + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell in + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + }) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(Item.all) + guard let dataSource = dataSource else { return } + dataSource.apply(snapshot) + } +} +extension ViewController { + func printBoxOfficeData() { + do { + + let endPoint = EndPoint( + baseURL: "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json", + queryItems: [ + "key": "d4bb1f8d42a3b440bb739e9d49729660", + "targetDt": "20230724" + ] + ) + + let url = try endPoint.generateURL(isFullPath: false) + + let urlRequest = URLRequest(url: url) + + let networkManager = NetworkManager() + networkManager.getBoxOfficeData(requestURL: urlRequest) { (boxOffice: BoxOffice) in + print(boxOffice) + } + + } catch { + print(error.localizedDescription) + } + } +} +extension ViewController { + func fetchBoxOfficeData(completion: @escaping () -> ()) { + do { + let endPoint = EndPoint( + baseURL: "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json", + queryItems: [ + "key": "d4bb1f8d42a3b440bb739e9d49729660", + "targetDt": "\(dateCountUpForTest)" + ] + ) + + let url = try endPoint.generateURL(isFullPath: false) + + let urlRequest = URLRequest(url: url) + + networkManager.getBoxOfficeData(requestURL: urlRequest) { (boxOffice: BoxOffice) in + let count = boxOffice.boxOfficeResult.dailyBoxOfficeList.count + for index in 0...(count-1) { + let rankNumber = boxOffice.boxOfficeResult.dailyBoxOfficeList[index].rankNumber + let rankIntensity = boxOffice.boxOfficeResult.dailyBoxOfficeList[index].rankIntensity + let movieName = boxOffice.boxOfficeResult.dailyBoxOfficeList[index].movieName + let audienceCount = boxOffice.boxOfficeResult.dailyBoxOfficeList[index].audienceCount + let audienceAccumulated = boxOffice.boxOfficeResult.dailyBoxOfficeList[index].audienceAccumulated + let rankOldAndNew = boxOffice.boxOfficeResult.dailyBoxOfficeList[index].rankOldAndNew + + let items = Item(rankNumber: rankNumber, rankIntensity: rankIntensity, movieName: movieName, audienceCount: audienceCount, audienceAccumulated: audienceAccumulated, rankOldAndNew: rankOldAndNew) + + Item.all.append(items) + } + completion() + } + } catch { + print(error.localizedDescription) + } + } } +extension ViewController { + private func initRefreshControl() { + refreshControl.addTarget(self, action: #selector(pullRefreshControl), for: .valueChanged) + collectionView.refreshControl = refreshControl + } + + @objc func pullRefreshControl() { + self.dateCountUpForTest += 1 + Item.all.removeAll() + fetchBoxOfficeData { + DispatchQueue.main.async { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(Item.all) + guard let dataSource = self.dataSource else { return } + dataSource.apply(snapshot) + + self.refreshControl.endRefreshing() + } + } + } +} + +extension ViewController { + private func fetchDate() { + do { + self.navigationItem.title = try DateFormatter().changeDateFormat(Date(), DateFormatter.DateFormat.hyphen) + } catch { + print(error.localizedDescription) + } + } +} + +extension ViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let detailViewController = DetailViewController() + navigationController?.pushViewController(detailViewController, animated: true) + } +} diff --git a/BoxOffice/Error/NetworkConfigurableError.swift b/BoxOffice/Error/NetworkConfigurableError.swift new file mode 100644 index 00000000..517e3b5a --- /dev/null +++ b/BoxOffice/Error/NetworkConfigurableError.swift @@ -0,0 +1,14 @@ +// +// NetworkConfigurableError.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/01. +// + +import Foundation + +enum NetworkConfigurableError: LocalizedError { + case urlComponents + case url + case urlResult +} diff --git a/BoxOffice/Error/NetworkManagerError.swift b/BoxOffice/Error/NetworkManagerError.swift new file mode 100644 index 00000000..86847d7c --- /dev/null +++ b/BoxOffice/Error/NetworkManagerError.swift @@ -0,0 +1,19 @@ +// +// NetworkManagerError.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/04. +// + +import Foundation + +enum NetworkManagerError: LocalizedError { + case decodedData + + var errorDescription: String? { + switch self { + case .decodedData: + return "데이터 디코딩에 실패했습니다." + } + } +} diff --git a/BoxOffice/Error/URLRequestError.swift b/BoxOffice/Error/URLRequestError.swift new file mode 100644 index 00000000..f4e3f84f --- /dev/null +++ b/BoxOffice/Error/URLRequestError.swift @@ -0,0 +1,19 @@ +// +// URLRequestError.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/07/28. +// + +import Foundation + +enum URLRequestError: LocalizedError { + case convertURL + + var errorDescription: String? { + switch self { + case .convertURL: + return "URL로 변경에 실패했습니다." + } + } +} diff --git a/BoxOffice/Extension/Bundle+.swift b/BoxOffice/Extension/Bundle+.swift new file mode 100644 index 00000000..f59be76f --- /dev/null +++ b/BoxOffice/Extension/Bundle+.swift @@ -0,0 +1,21 @@ +// +// Bundle+.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/01. +// + +import Foundation + +extension Bundle { + var API: String { + guard let file = self.path(forResource: "API_KEY", ofType: "plist") else { return "" } + + guard let resource = NSDictionary(contentsOfFile: file) else { return "" } + + guard let apiKEY = resource["key"] as? String else { + fatalError("API error") + } + return apiKEY + } +} diff --git a/BoxOffice/Extension/DateFormatter+.swift b/BoxOffice/Extension/DateFormatter+.swift new file mode 100644 index 00000000..e33ad5d7 --- /dev/null +++ b/BoxOffice/Extension/DateFormatter+.swift @@ -0,0 +1,23 @@ +// +// DateFormatter+.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/11. +// + +import Foundation + +extension DateFormatter { + func changeDateFormat(_ date: Date, _ format: String) throws -> String { + self.dateFormat = format + + return self.string(from: date) + } +} + +extension DateFormatter { + enum DateFormat { + static let hyphen = "yyyy-MM-dd" + static let attached = "yyyyMMdd" + } +} diff --git a/BoxOffice/Extension/String+.swift b/BoxOffice/Extension/String+.swift new file mode 100644 index 00000000..fc7df30b --- /dev/null +++ b/BoxOffice/Extension/String+.swift @@ -0,0 +1,17 @@ +// +// String+.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/12. +// + +import Foundation + +extension String { + func changeNumberFormat() -> String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + return numberFormatter.string(for: Double(self)) ?? "Error" + } +} diff --git a/BoxOffice/Extension/UICellConfigurationState+.swift b/BoxOffice/Extension/UICellConfigurationState+.swift new file mode 100644 index 00000000..80b0888d --- /dev/null +++ b/BoxOffice/Extension/UICellConfigurationState+.swift @@ -0,0 +1,15 @@ +// +// UICellConfigurationState+.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/06. +// + +import UIKit + +extension UICellConfigurationState { + var item: Item? { + get { return self[.item] as? Item } + set { self[.item] = newValue} + } +} diff --git a/BoxOffice/Extension/UIConfigurationStateCustomKey+.swift b/BoxOffice/Extension/UIConfigurationStateCustomKey+.swift new file mode 100644 index 00000000..2bb01652 --- /dev/null +++ b/BoxOffice/Extension/UIConfigurationStateCustomKey+.swift @@ -0,0 +1,12 @@ +// +// UIConfigurationStateCustomKey+.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/06. +// + +import UIKit + +extension UIConfigurationStateCustomKey { + static let item = UIConfigurationStateCustomKey("dailyBoxOfficeList") +} diff --git a/BoxOffice/Info.plist b/BoxOffice/Info.plist index dd3c9afd..c0ae0b42 100644 --- a/BoxOffice/Info.plist +++ b/BoxOffice/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/BoxOffice/Model/BoxOffice.swift b/BoxOffice/Model/BoxOffice.swift index d62b0a23..731e9ebe 100644 --- a/BoxOffice/Model/BoxOffice.swift +++ b/BoxOffice/Model/BoxOffice.swift @@ -5,11 +5,11 @@ // Created by Kobe, yyss99 on 2023/07/25. // -struct BoxOffice: Decodable { +struct BoxOffice: Decodable, Hashable { let boxOfficeResult: BoxOfficeResult } -struct BoxOfficeResult: Decodable { +struct BoxOfficeResult: Decodable, Hashable { let boxOfficeType: String let showRange: String let dailyBoxOfficeList: [DailyBoxOfficeList] @@ -21,7 +21,7 @@ struct BoxOfficeResult: Decodable { } } -struct DailyBoxOfficeList: Decodable { +struct DailyBoxOfficeList: Decodable, Hashable { let rankNumber: String let rank: String let rankIntensity: String diff --git a/BoxOffice/Model/EndPoint.swift b/BoxOffice/Model/EndPoint.swift new file mode 100644 index 00000000..89f40dab --- /dev/null +++ b/BoxOffice/Model/EndPoint.swift @@ -0,0 +1,11 @@ +// +// EndPoint.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/01. +// + +public struct EndPoint: NetworkConfigurable { + public var baseURL: String + public var queryItems: [String : String]? +} diff --git a/BoxOffice/Model/Item.swift b/BoxOffice/Model/Item.swift new file mode 100644 index 00000000..76d94039 --- /dev/null +++ b/BoxOffice/Model/Item.swift @@ -0,0 +1,30 @@ +// +// Item.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/10. +// + +import Foundation + +struct Item: Hashable { + let rankNumber: String? + let rankIntensity: String? + let movieName: String? + let audienceCount: String? + let audienceAccumulated: String? + let rankOldAndNew: String? + + init(rankNumber: String? = nil, rankIntensity: String? = nil, movieName: String? = nil, audienceCount: String? = nil, audienceAccumulated: String? = nil, rankOldAndNew: String? = nil) { + self.rankNumber = rankNumber + self.rankIntensity = rankIntensity + self.movieName = movieName + self.audienceCount = audienceCount + self.audienceAccumulated = audienceAccumulated + self.rankOldAndNew = rankOldAndNew + } + + private let identifier = UUID() + + static var all: [Item] = [] +} diff --git a/BoxOffice/Model/NetworkConfigurable.swift b/BoxOffice/Model/NetworkConfigurable.swift new file mode 100644 index 00000000..b21b97ac --- /dev/null +++ b/BoxOffice/Model/NetworkConfigurable.swift @@ -0,0 +1,65 @@ +// +// NetworkConfigurable.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/01. +// + +import Foundation + +public protocol NetworkConfigurable { + var baseURL: String { get } + var queryItems: [String: String]? { get } +} + +extension NetworkConfigurable { + public func generateURLRequest() throws -> URLRequest { + var urlQureyItems: [URLQueryItem] = [] + + guard var urlComponents = URLComponents(string: baseURL) else { + throw NetworkConfigurableError.urlComponents + } + + queryItems?.forEach { + urlQureyItems.append(URLQueryItem(name: $0.key, value: $0.value)) + } + + urlComponents.queryItems = urlQureyItems + + guard let url = urlComponents.url else { + throw NetworkConfigurableError.url + } + + let requestURL = URLRequest(url: url) + + return requestURL + } + + public func generateURL(isFullPath: Bool) throws -> URL { + guard let url = URL(string: baseURL) else { + throw NetworkConfigurableError.url + } + + let baseURL = url.absoluteString + + guard var urlComponents = URLComponents(string: baseURL) else { + throw NetworkConfigurableError.urlComponents + } + + var urlQureyItems: [URLQueryItem] = [] + + queryItems?.forEach { + urlQureyItems.append(URLQueryItem(name: $0.key, value: $0.value)) + } + + urlComponents.queryItems = !urlQureyItems.isEmpty ? urlQureyItems : nil + + let fullPathURL = isFullPath ? URL(string: baseURL) : urlComponents.url + + guard let urlResult = fullPathURL else { + throw NetworkConfigurableError.urlResult + } + + return urlResult + } +} diff --git a/BoxOffice/Model/NetworkManager.swift b/BoxOffice/Model/NetworkManager.swift new file mode 100644 index 00000000..5c00b7db --- /dev/null +++ b/BoxOffice/Model/NetworkManager.swift @@ -0,0 +1,42 @@ +// +// NetworkManager.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/07/28. +// + +import Foundation + +public protocol Gettable { + func getBoxOfficeData(requestURL: URLRequest, completionHandler: @escaping (T) -> Void) +} + +struct NetworkManager: Gettable { + func getBoxOfficeData(requestURL: URLRequest, completionHandler: @escaping (T) -> Void) { + let task = URLSession.shared.dataTask(with: requestURL) { data, response, error in + guard error == nil else { + return + } + + let successStatusRange = 200..<300 + guard let response = response as? HTTPURLResponse, + successStatusRange.contains(response.statusCode) else { + return + } + + guard let data = data else { + return + } + + do { + let receivedData = try JSONDecoder().decode(T.self, from: data) + completionHandler(receivedData) + } catch let error as NetworkManagerError { + print(error.localizedDescription) + } catch { + print(error.localizedDescription) + } + } + task.resume() + } +} diff --git a/BoxOffice/Model/RankIntensity.swift b/BoxOffice/Model/RankIntensity.swift new file mode 100644 index 00000000..211ece68 --- /dev/null +++ b/BoxOffice/Model/RankIntensity.swift @@ -0,0 +1,80 @@ +// +// RankIntensity.swift +// BoxOffice +// +// Created by by Kobe, yyss99 on 2023/08/16. +// + +import UIKit + +enum RankIntensity { + case up(Int) + case down(Int) + case stay + + init?(fromString string: String) { + if let value = Int(string), value > 0 { + self = .up(value) + } else if let value = Int(string), value < 0 { + self = .down(value) + } else if string == "0" { + self = .stay + } else { + return nil + } + } + + func attributedString(withFont font: UIFont) -> NSAttributedString? { + var attributedString: NSMutableAttributedString + let numberAttributedString: NSMutableAttributedString + + let returnAttributedString: NSMutableAttributedString = NSMutableAttributedString() + switch self { + case .up(let value): + attributedString = NSMutableAttributedString(string: "▲") + attributedString.addAttributes([ + .foregroundColor: UIColor.red, + .font: font + ], range: NSRange(location: 0, length: attributedString.length)) + + numberAttributedString = NSMutableAttributedString(string: "\(value)") + numberAttributedString.addAttributes([ + .foregroundColor: UIColor.black, + .font: font + ], range: NSRange(location: 0, length: numberAttributedString.length)) + + returnAttributedString.append(attributedString) + returnAttributedString.append(numberAttributedString) + + case .down(let value): + attributedString = NSMutableAttributedString(string: "▼") + attributedString.addAttributes([ + .foregroundColor: UIColor.blue, + .font: font + ], range: NSRange(location: 0, length: attributedString.length)) + + let valueStr = String(value) + if let numberOnly = valueStr.last { + numberAttributedString = NSMutableAttributedString(string: "\(numberOnly)") + numberAttributedString.addAttributes([ + .foregroundColor: UIColor.black, + .font: font + ], range: NSRange(location: 0, length: numberAttributedString.length)) + + returnAttributedString.append(attributedString) + returnAttributedString.append(numberAttributedString) + } + + case .stay: + attributedString = NSMutableAttributedString(string: "-") + attributedString.addAttributes([ + .foregroundColor: UIColor.black, + .font: font + ], range: NSRange(location: 0, length: attributedString.length)) + + returnAttributedString.append(attributedString) + } + + return returnAttributedString + } +} diff --git a/BoxOffice/Model/Section.swift b/BoxOffice/Model/Section.swift new file mode 100644 index 00000000..610cfcd0 --- /dev/null +++ b/BoxOffice/Model/Section.swift @@ -0,0 +1,10 @@ +// +// Section.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/06. +// + +enum Section { + case main +} diff --git a/BoxOffice/Model/URLSession.swift b/BoxOffice/Model/URLSession.swift new file mode 100644 index 00000000..ea477328 --- /dev/null +++ b/BoxOffice/Model/URLSession.swift @@ -0,0 +1,8 @@ +// +// URLSession.swift +// BoxOffice +// +// Created by Yeseul Jang on 2023/07/26. +// + +import Foundation diff --git a/BoxOffice/View/Base.lproj/Main.storyboard b/BoxOffice/View/Base.lproj/Main.storyboard index 25a76385..6df16562 100644 --- a/BoxOffice/View/Base.lproj/Main.storyboard +++ b/BoxOffice/View/Base.lproj/Main.storyboard @@ -1,24 +1,52 @@ - + + - + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BoxOffice/View/CustomListCell.swift b/BoxOffice/View/CustomListCell.swift new file mode 100644 index 00000000..9a59ecdb --- /dev/null +++ b/BoxOffice/View/CustomListCell.swift @@ -0,0 +1,118 @@ +// +// CustomListCell.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/08. +// + +import UIKit + +class CustomListCell: ItemListCell { + private func defaultListContentConfiguration() -> UIListContentConfiguration { + return .subtitleCell() + } + + private lazy var listContentView = UIListContentView(configuration: defaultContentConfiguration()) + + let rankNumberLabel = UILabel() + let rankChangeLabel = UILabel() + + var setupViewsIfNeededFlag: Bool? = nil +} + +extension CustomListCell { + func setupViewsIfNeeded() { + let movieRankStackView = UIStackView(arrangedSubviews: [rankNumberLabel, rankChangeLabel]) + movieRankStackView.alignment = .center + movieRankStackView.distribution = .fillProportionally + movieRankStackView.spacing = 0 + movieRankStackView.axis = .vertical + movieRankStackView.translatesAutoresizingMaskIntoConstraints = false + + [listContentView, movieRankStackView].forEach { + contentView.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + let contentViewHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 100) + contentViewHeightConstraint.priority = .defaultHigh + + NSLayoutConstraint.activate([ + contentViewHeightConstraint, + listContentView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60), + listContentView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + movieRankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30), + movieRankStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) + + setupViewsIfNeededFlag = true + } + + override func updateConfiguration(using state: UICellConfigurationState) { + setupViewsIfNeeded() + + var content = defaultListContentConfiguration().updated(for: state) + + content.text = state.item?.movieName + content.textProperties.font = .preferredFont(forTextStyle: .subheadline) + content.secondaryText = "오늘 \(state.item?.audienceCount?.changeNumberFormat() ?? "오류") / 총 \(state.item?.audienceAccumulated?.changeNumberFormat() ?? "오류")" + + rankNumberLabel.text = state.item?.rankNumber + rankNumberLabel.font = .preferredFont(forTextStyle: .title1) + + guard let isNewMovie = state.item?.rankOldAndNew else { return } + guard let rankIntensity = state.item?.rankIntensity else { return } + + if isNewMovie == "NEW" { + makeOldAndNewMovieCategory(isNewMovie: isNewMovie) { [weak self] oldAndNewMovieTitle in + self?.rankChangeLabel.attributedText = oldAndNewMovieTitle + } + } else if isNewMovie == "OLD" { + makeRankIntensity(rankIntensityData: rankIntensity) { [weak self] rankIntensity in + self?.rankChangeLabel.attributedText = rankIntensity + } + } + + listContentView.configuration = content + } +} + +extension CustomListCell { + func makeRankIntensity(rankIntensityData: String?, completion: @escaping (NSAttributedString) -> Void) { + guard let rankIntensityData = rankIntensityData else { return } + guard let rankIntensity = RankIntensity(fromString: rankIntensityData) else { return } + let font = UIFont.preferredFont(forTextStyle: .caption1) + guard let attributedString = rankIntensity.attributedString(withFont: font) else { return } + completion(attributedString) + } +} + +extension CustomListCell { + func makeOldAndNewMovieCategory(isNewMovie: String, completion: @escaping (NSAttributedString) -> Void) { + var oldAndNewMovieCategory: String { + switch isNewMovie { + case "NEW": + return "신작" + case "OLD": + return "기존" + default: + return "Error" + } + } + + let font = UIFont.systemFont(ofSize: 17.0) + + if oldAndNewMovieCategory == "신작" { + let attributeOldAndNewMovieCategory = NSMutableAttributedString(string: oldAndNewMovieCategory) + + attributeOldAndNewMovieCategory.addAttributes([ + NSMutableAttributedString.Key.foregroundColor: UIColor.red, + NSMutableAttributedString.Key.font: font as Any + ], range: (oldAndNewMovieCategory as NSString).range(of: oldAndNewMovieCategory)) + + completion(attributeOldAndNewMovieCategory) + } + } +} + + diff --git a/BoxOffice/View/ItemListCell.swift b/BoxOffice/View/ItemListCell.swift new file mode 100644 index 00000000..f940f2d6 --- /dev/null +++ b/BoxOffice/View/ItemListCell.swift @@ -0,0 +1,26 @@ +// +// ItemListCell.swift +// BoxOffice +// +// Created by Kobe, yyss99 on 2023/08/08. +// + +import UIKit + +class ItemListCell: UICollectionViewListCell { + var item: Item? = nil + + override var configurationState: UICellConfigurationState { + var state = super.configurationState + state.item = self.item + return state + } + + func updateWithItem(_ newItem: Item) { + guard item != newItem else { return } + + item = newItem + + setNeedsUpdateConfiguration() + } +} diff --git a/README.md b/README.md index bb826d67..481f6cdf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,252 @@ -## 박스오피스 프로젝트 저장소 +# 박스오피스[STEP3] -이 저장소를 자신의 저장소로 fork하여 프로젝트를 진행합니다! +> 📌 네트워킹을 활용하여 박스오피스 데이터를 불러와 정보를 화면에 표기하고 리스트를 잡아끌면 새로고침할 수 있도록 하는 기능이 있는 앱입니다. + +## 📚 목차
+- [팀원소개](#-팀원-소개) +- [파일트리](#-파일트리) +- [시각화된 프로젝트 구조](#시각화된-프로젝트-구조) +- [타임라인](#-타임라인) +- [실행화면](#-실행화면) +- [트러블 슈팅](#-트러블-슈팅) +- [참고자료](#-참고자료) + +## 🧑‍💻 팀원 소개
+| | | +| :-: | :-: | +| [**yyss99(와이)**](https://github.com/yy-ss99) | [**Kobe**](https://github.com/devKobe24) | + +## 🗂️ 파일트리
+``` +. +. +├── BoxOffice +│   ├── API_KEY.plist +│   ├── Model +│   │   ├── BoxOffice.swift +│   │   ├── EndPoint.swift +│   │   ├── Item.swift +│   │   ├── NetworkConfigurable.swift +│   │   ├── NetworkManager.swift +│   │   ├── Section.swift +│   │   └── URLSession.swift +│   ├── View +│   │ ├── Base.lproj +│   │ │   ├── LaunchScreen.storyboard +│   │ │   └── Main.storyboard +│   │ ├── CustomListCell.swift +│   │ └── ItemListCell.swift +│   ├── Controller +│   │   └── ViewController.swift +│   ├── Error +│   │   ├── NetworkConfigurableError.swift +│   │   ├── NetworkManagerError.swift +│   │   └── URLRequestError.swift +│   ├── Extension +│   │   ├── Bundle+.swift +│   │   ├── UICellConfigurationState+.swift +│   │   └── UIConfigurationStateCustomKey+.swift +│   ├── Info.plist +│   ├── Resource +│   │   ├── AppDelegate.swift +│   │   └── SceneDelegate.swift +├── BoxOfficeTests +│   ├── BoxOfficeTestPlan.xctestplan +│   └── BoxOfficeTests.swift +└── README.md +``` + +## 🗺️ 시각화된 프로젝트 구조
+ + +## ⏰ 타임라인
+프로젝트 진행 기간 | 23.07.10.(월) ~ 23.07.14.(금) + +| 날짜 | 진행 사항 | +| -------- | -------- | +| 23.07.25.(월) | BoxOffice 모델 객체 생성 및 구현.
JSON 객체 추가 및 BoxOffice 모델 생성 및 구현.
BoxOfficeModel에 코딩키 추가.
BoxOffice Model Unit Test 생성 및 실행.
refactor: MVC 패턴으로 폴더링.
| +| 23.07.26.(화) | 인덴트 수정.
+| 23.07.27.(수) | NetworkManager 구현 및 MakeURLRequestError 오류 타입 구현.
+| 23.08.01(화) | NetworkConfigurable 프로토콜 구현 및 확장.
NetworkConfigurable에 generateURL 메서드 추가.
NetworkConfigurableError 타입 구현.
App Transport Security Settings Key를 추가.
API_KEY gitignore 추가.
Bundle 확장 및 내부 구현.
BoxOffice 데이터 출력을 위한 함수 구현.
+| 23.08.01(화) | 리드미 업데이트.
NetWorkManagerError 생성.
+| 23.08.06(일) | Section 모델 구현.
UIConfigurationStateCustomKey 확장 및 구현.
UICellConfigurationState 확장.
+| 23.08.08(화) |UICellConfigurationState에 Item 추가.
boxOffice 구조체에 Hashable 포로토콜 채택.
UICellConfigurationState에 Item 추가.
CustomListCell 생성.
ItemListCell로 파일명 변경.
CustomListCell 구현.
configureDataSource, creatLayout 함수 구현 +| 23.08.10(목) | Item 타입 생성.
Item 타입으로 수정.
generateURL 함수 수정.
fetchBoxOfficeData 함수 생성, Item으로 수정.
CustomListCell Layout 수정.
ViewController refresh 기능 구현, navigationController 추가. + +## 📺 실행화면
+- STEP3 BoxOffice 시뮬레이터 실행화면 🎬
+ + +## 🔨 트러블 슈팅 +### 1️⃣ **오늘 날짜로 targetDt를 parameter로 설정하고 GET을 SEND했을 경우 데이터가 빈값으로 들어옵니다.**
+### 🔒 **문제점** 🔒
+
+ +**🚨 `postman`으로 확인해본 결과 `targetDt`를 오늘 날자로 설정하고 `GET`으로 요청을 보냈을 경우 빈 데이터가 돌아오는 것을 확인 할 수 있었습니다.**
+ +### 🔑 **해결방법** 🔑
+**🙋‍♂️ 그러므로 화면 상단 네비게이션 타이틀에는 현재 날짜를 보여주게 만들고
실제 통신하여 가져오는 데이터는 오늘로부터 24시간 전
전날의 데이터를 가져와서 화면에 보여지게 만들려고 계획중이며 코드는 아래와 같이 구성할 예정입니다.** + +
+ FetchDateable Model + +```swift! +import Foundation + +protocol FetchDateble { + +} + +extension FetchDateble { + func fetchDate() -> String { + let currentDate = Date() + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "ko_KR") + dateFormatter.dateFormat = "yyyy-MM-dd" + let fetchDate = dateFormatter.string(from: currentDate) + + return fetchDate + } + + func fetchDateForTargetDt() throws -> String { + let currentDate = Date() + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "ko_KR") + dateFormatter.dateFormat = "yyyyMMdd" + let calener = Calendar.current + let yesterDay = calener.date(byAdding: .day, value: -1, to: currentDate) + guard let yesterDay = yesterDay else { + throw FetchDateForTargetDtError.unwrap + } + let fetchDate = dateFormatter.string(from: yesterDay) + + return fetchDate + } +} + +enum FetchDateForTargetDtError: Error { + case unwrap +} +``` +
+ +
+ ViewController + +```swift! +class ViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + + self.configureHierarchy() + self.initRefreshControl() + + fetchBoxOfficeData { + DispatchQueue.main.async { + self.configureDataSource() + self.setupNavigationTitle() + } + } + } +} +``` +
+ +
+ ViewController Extension + +```swift! +extension ViewController: FetchDateble { + private func setupNavigationTitle() { + navigationItem.title = fetchDate() + } +} +``` +
+
+ +### 2️⃣ **화면을 새로고침을 할 때 새로운 데이터가 들어올 경우 데이터를 업데이트 해주는 방법.**
+### 🔒 **문제점** 🔒
+**🚨 화면을 땅겨서 새로고침 액션을 실행할 경우, 새로운 데이터가 들어오는 경우에도 불구하고 화면에 새로운 데이터가 새롭게 업데이트 되지 않는 문제점이 있었습니다.** + +### 🔑 **해결방법** 🔑
+`UICollectionView.CellRegistration` 타입의 `CellRegistration`을 만들고 커스텀하게 `UICollectionViewListCell` 타입의 `ItemListCell` 이름으로 클래스를 만들어 줍니다.
+ +`ItemListCell` 내부에 `updateWithItem(_:)` 함수를 구현하여 이 함수로 새로운 데이터가 들어올 경우 아이템을 업데이트하게 합니다.
+ +`UICollectionViewDiffableDataSource` 타입의 데이터소스를 만들고 `return` 값으로 `collectionView`의 `dequeueConfiguredReusableCell(using:for:item:)`을 활용하여 각각의 `parameter`에 맞는 값을 넣어줍니다.
+ +특히 `using` 파라미터에는 위에서 만든 `cellRegistration`을 넣어줍니다.
+ +이후 `NSDiffableDataSourceSnapshot` 타입의 스냅샷을 만들어 `SectionIdentifierType`에는 미리 만들어 놓은 `Seciotn` 열거형을 넣어주고 `ItemIdentifierType`에는 `Item` 구조체를 넣어주어 `snapshot` 인스턴스를 만듭니다.
+ +`snapshot.append(Item.all)`을 구현하고 `snapshot` 인스턴스와 `dataSource` 인스턴스를 활용하여 `dataSource.apply(snapshot)`을 구현하여 새로운 데이터가 들어올 경우 데이터를 업데이트해 줄 수 있도록 하였습니다.
+ +### 3️⃣ CollectionListView에서 임의로 높이를 주기
+### 🔒 **문제점** 🔒
+ +셀의 위아래 여백을 주기 위해 cell의 constraint를 잡았으나 Layout 오류가 발생 했습니다. 높이를 따로 지정해 주지 않았는데도 불구하고 Height의 값이 44로 잡혀 있다는 오류가 발생했습니다. 검색 결과, 자동적으로 cell 높이를 부여해준다는 사실을 알았습니다. + +```swift! +NSLayoutConstraint.activate([ + listContentView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60), + listContentView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 30), + listContentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + listContentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30), + + movieRankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30), + movieRankStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 30), + movieRankStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30) + ]) +``` +### 🔑 **해결방법** 🔑
+ +1. 첫번째 해결 방법 - `listContentView` 위 아래 공백에 더 큰 `priority`주기 + + `cell`의 자동적으로 정해져 있는 `height`보다 `listContentView`의 아래 공백을 조정하는 `constraint`에 높은 `priority`를 줍니다. 이런 식으로 하면 자동적으로 잡혀있던 `cell`의 `height` 값이 무시되면서 강제로 `listContentView` 위 아래로 공백을 잡아주어서 코드로 잡아준 `layout`이 적용됩니다. + +```swift! +let listContentViewBottomConstraint = listContentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30) + listContentViewBottomConstraint.priority = .defaultHigh + let movieRankStackViewBottomConstraint = movieRankStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30) + movieRankStackViewBottomConstraint.priority = .defaultHigh + + NSLayoutConstraint.activate([ + listContentView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60), + listContentView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 30), + listContentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + listContentViewBottomConstraint, + + movieRankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30), + movieRankStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 30), + movieRankStackViewBottomConstraint + ]) +``` +2. 두번째 해결 방법 - `cell` 자체의 `height`를 고정해주기 + `cell`에 접근해서 `cell` 자체의 `height`를 결정해주고 이 `cell`의 `height`에 높은 `priority`를 줍니다. 자동적으로 잡혀있던 `cell`의 `height`보다 `code`로 준 `height`가 적용됩니다. + +```swift! +let contentViewHeightConstraint = contentView.heightAnchor.constraint(equalToConstant: 100) + contentViewHeightConstraint.priority = .defaultHigh + + NSLayoutConstraint.activate([ + contentViewHeightConstraint, + listContentView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60), + listContentView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + movieRankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 30), + movieRankStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) +``` + +## 📑 참고자료 +- [📃 snapshot()](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/3255141-snapshot) +- [📃 UICollectionViewListCell](https://developer.apple.com/documentation/uikit/uicollectionviewlistcell) +- [📃 UICollectionView.CellRegistration](https://developer.apple.com/documentation/uikit/uicollectionview/cellregistration) +- [📃 UICollectionViewDiffableDataSource](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource) +- [📃 UICollectionViewLayout](https://developer.apple.com/documentation/uikit/uicollectionviewlayout) +- [📃 UICollectionViewCompositionalLayout](https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout) +- [📃 UICollectionView](https://developer.apple.com/documentation/uikit/uicollectionview) + - [🎦 Modern cell configuration](https://developer.apple.com/videos/play/wwdc2020/10027/) + - [🎦 Lists in UICollectionView](https://developer.apple.com/videos/play/wwdc2020/10026) + - [📃 Implementing Modern Collection Views](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views)