From 6ccec13f37a01311ac67234fe018d796439f5e4d Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 13 Jul 2023 11:16:23 +0900 Subject: [PATCH 01/17] =?UTF-8?q?file:=20=EC=97=AD=ED=95=A0=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GameScope/GameScope.xcodeproj/project.pbxproj | 4 ++-- .../GameScope/{ => Controller}/DetailViewController.swift | 0 GameScope/GameScope/{ => View}/HeaderView.swift | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename GameScope/GameScope/{ => Controller}/DetailViewController.swift (100%) rename GameScope/GameScope/{ => View}/HeaderView.swift (100%) diff --git a/GameScope/GameScope.xcodeproj/project.pbxproj b/GameScope/GameScope.xcodeproj/project.pbxproj index f7fc3b6..1e0017c 100644 --- a/GameScope/GameScope.xcodeproj/project.pbxproj +++ b/GameScope/GameScope.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ F15550702A4EDCEC00832E92 /* View */ = { isa = PBXGroup; children = ( + F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */, F15550712A4EDD0400832E92 /* gameListCollectionViewCell.swift */, ); path = View; @@ -106,6 +107,7 @@ F15550732A4EE20F00832E92 /* Controller */ = { isa = PBXGroup; children = ( + F6B45FD72A500F5200F1D0DB /* DetailViewController.swift */, F14B01752A513FEF00D43B1D /* ViewController.swift */, ); path = Controller; @@ -134,8 +136,6 @@ children = ( F1E20C722A4D6E1B00772B80 /* AppDelegate.swift */, F1E20C742A4D6E1B00772B80 /* SceneDelegate.swift */, - F6B45FD72A500F5200F1D0DB /* DetailViewController.swift */, - F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */, F1E20C782A4D6E1B00772B80 /* Main.storyboard */, F1E20C7B2A4D6E1C00772B80 /* Assets.xcassets */, F1E20C7D2A4D6E1C00772B80 /* LaunchScreen.storyboard */, diff --git a/GameScope/GameScope/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift similarity index 100% rename from GameScope/GameScope/DetailViewController.swift rename to GameScope/GameScope/Controller/DetailViewController.swift diff --git a/GameScope/GameScope/HeaderView.swift b/GameScope/GameScope/View/HeaderView.swift similarity index 100% rename from GameScope/GameScope/HeaderView.swift rename to GameScope/GameScope/View/HeaderView.swift From d9f001ccdced96eac7dbc1a7522b65940ca43104 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 13 Jul 2023 11:49:02 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20DiffableDataSource=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/DetailViewController.swift | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/GameScope/GameScope/Controller/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift index 260ac6f..2da0372 100644 --- a/GameScope/GameScope/Controller/DetailViewController.swift +++ b/GameScope/GameScope/Controller/DetailViewController.swift @@ -19,9 +19,6 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { case screenshots = "ScreenShots" } - private typealias DataSource = UICollectionViewDiffableDataSource - - private lazy var dataSource = configureDataSource() private lazy var detailCollectionView = { let collectionView = UICollectionView( frame: view.bounds, @@ -55,10 +52,10 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { extension DetailViewController { - private func configureDataSource() -> DataSource { - let dataSource = DataSource(collectionView: detailCollectionView) { collectionView, indexPath, item in - let sectionType = Section.allCases[indexPath.section] - switch sectionType { + private func generateLayout() -> UICollectionViewLayout { + let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in + let sectionLayoutKind = Section.allCases[sectionIndex] + switch sectionLayoutKind { case .thumbnail: return nil case .about: @@ -69,37 +66,29 @@ extension DetailViewController { return nil } } + return layout + } - dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in - guard let supplementaryView = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: HeaderView.reuseIdentifier, - for: indexPath - ) as? HeaderView else { - fatalError("Cannot create header view") - } - supplementaryView.label.text = Section.allCases[indexPath.section].rawValue - return supplementaryView - } +} + +extension DetailViewController: UICollectionViewDataSource { - return dataSource + func numberOfSections(in collectionView: UICollectionView) -> Int { + Section.allCases.count } - func generateLayout() -> UICollectionViewLayout { - let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in - let sectionLayoutKind = Section.allCases[sectionIndex] - switch sectionLayoutKind { - case .thumbnail: - return nil - case .about: - return nil - case .information: - return nil - case .screenshots: - return nil - } + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + switch Section.allCases[section] { + default: + return 0 + } + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + switch Section.allCases[indexPath.section] { + default: + return UICollectionViewCell() } - return layout } } From 78de130e31b92de16ffc1c09a01fdb1dabb5efc5 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 13 Jul 2023 18:16:01 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20InformationView=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타입에 맞는 title과 information을 적는 Label로 구성 --- GameScope/GameScope.xcodeproj/project.pbxproj | 8 ++++++ .../GameScope/View/InformationView.swift | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 GameScope/GameScope/View/InformationView.swift diff --git a/GameScope/GameScope.xcodeproj/project.pbxproj b/GameScope/GameScope.xcodeproj/project.pbxproj index 1e0017c..67236e0 100644 --- a/GameScope/GameScope.xcodeproj/project.pbxproj +++ b/GameScope/GameScope.xcodeproj/project.pbxproj @@ -37,6 +37,8 @@ F6B45FD62A4EC6A000F1D0DB /* wrong-game-pubg.json in Resources */ = {isa = PBXBuildFile; fileRef = F6B45FD22A4EBF4A00F1D0DB /* wrong-game-pubg.json */; }; F6B45FD82A500F5200F1D0DB /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FD72A500F5200F1D0DB /* DetailViewController.swift */; }; F6B45FDF2A57A10D00F1D0DB /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */; }; + F6B45FEA2A5F962200F1D0DB /* GameDetailInformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */; }; + F6B45FEC2A5F97DF00F1D0DB /* InformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -74,6 +76,8 @@ F6B45FD42A4EC5D900F1D0DB /* GameDetailDTOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailDTOTests.swift; sourceTree = ""; }; F6B45FD72A500F5200F1D0DB /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; + F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailInformationCell.swift; sourceTree = ""; }; + F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -100,6 +104,8 @@ children = ( F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */, F15550712A4EDD0400832E92 /* gameListCollectionViewCell.swift */, + F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */, + F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */, ); path = View; sourceTree = ""; @@ -301,11 +307,13 @@ F6B45FAE2A4E752900F1D0DB /* GameDetailDTO.swift in Sources */, F6B45FB02A4E768B00F1D0DB /* MinimumSystemRequirementsDTO.swift in Sources */, F1E20C752A4D6E1B00772B80 /* SceneDelegate.swift in Sources */, + F6B45FEC2A5F97DF00F1D0DB /* InformationView.swift in Sources */, F6B45FB22A4E775100F1D0DB /* ScreenshotDTO.swift in Sources */, F6B45FDF2A57A10D00F1D0DB /* HeaderView.swift in Sources */, F6B45FD82A500F5200F1D0DB /* DetailViewController.swift in Sources */, F6B45FB92A4EA9E700F1D0DB /* GameList.swift in Sources */, F6B45FAC2A4E71DA00F1D0DB /* GameListDTO.swift in Sources */, + F6B45FEA2A5F962200F1D0DB /* GameDetailInformationCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GameScope/GameScope/View/InformationView.swift b/GameScope/GameScope/View/InformationView.swift new file mode 100644 index 0000000..3721f99 --- /dev/null +++ b/GameScope/GameScope/View/InformationView.swift @@ -0,0 +1,28 @@ +// +// InformationView.swift +// GameScope +// +// Created by 박재우 on 2023/07/13. +// + +import UIKit + +class InformationView: UIStackView { + + let titleLabel = UILabel() + let informationLabel = UILabel() + + override func layoutSubviews() { + addArrangedSubview(titleLabel) + addArrangedSubview(informationLabel) + + axis = .vertical + distribution = .fillEqually + + titleLabel.font = .preferredFont(forTextStyle: .title3) + titleLabel.textColor = #colorLiteral(red: 0.3450980392, green: 0.3450980392, blue: 0.3450980392, alpha: 1) + + informationLabel.font = .preferredFont(forTextStyle: .title3) + } + +} From f02d35ff787494eac6bc92161b9d2d2c03c43b0c Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 13 Jul 2023 18:17:18 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20GameDetailInformationCell=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 각 타입마다 InformationView로 표현되도록 설정 --- .../View/GameDetailInformationCell.swift | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 GameScope/GameScope/View/GameDetailInformationCell.swift diff --git a/GameScope/GameScope/View/GameDetailInformationCell.swift b/GameScope/GameScope/View/GameDetailInformationCell.swift new file mode 100644 index 0000000..15f8090 --- /dev/null +++ b/GameScope/GameScope/View/GameDetailInformationCell.swift @@ -0,0 +1,83 @@ +// +// GameDetailInformationCell.swift +// GameScope +// +// Created by 박재우 on 2023/07/13. +// + +import UIKit + +class GameDetailInformationCell: UICollectionViewCell { + + static let reuseIdentifier = "GameDetailInformationCell" + + enum Information: String { + case genre = "Genre" + case developer = "Developer" + case releaseDate = "Release Date" + case publisher = "Publisher" + } + + private let genreInformationView: InformationView = { + let informationView = InformationView() + informationView.titleLabel.text = Information.genre.rawValue + return informationView + }() + private let developerInformationView: InformationView = { + let informationView = InformationView() + informationView.titleLabel.text = Information.developer.rawValue + return informationView + }() + private let releaseDateInformationView: InformationView = { + let informationView = InformationView() + informationView.titleLabel.text = Information.releaseDate.rawValue + return informationView + }() + private let publisherInformationView: InformationView = { + let informationView = InformationView() + informationView.titleLabel.text = Information.publisher.rawValue + return informationView + }() + + override func layoutSubviews() { + backgroundColor = #colorLiteral(red: 0.9568627451, green: 0.9568627451, blue: 0.9568627451, alpha: 1) + + addSubview(genreInformationView) + addSubview(developerInformationView) + addSubview(releaseDateInformationView) + addSubview(publisherInformationView) + + genreInformationView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(5) + make.leading.equalToSuperview().inset(15) + make.trailing.equalTo(snp.centerX).inset(5) + make.bottom.equalTo(snp.centerY).offset(-5) + } + developerInformationView.snp.makeConstraints { make in + make.top.equalToSuperview().inset(5) + make.leading.equalTo(snp.centerX).inset(5) + make.trailing.equalToSuperview().inset(10) + make.bottom.equalTo(snp.centerY).offset(-5) + } + releaseDateInformationView.snp.makeConstraints { make in + make.top.equalTo(snp.centerY).offset(5) + make.leading.equalToSuperview().inset(15) + make.trailing.equalTo(snp.centerX).inset(5) + make.bottom.equalToSuperview().inset(5) + } + publisherInformationView.snp.makeConstraints { make in + make.top.equalTo(snp.centerY).offset(5) + make.leading.equalTo(snp.centerX).inset(5) + make.trailing.equalToSuperview().inset(10) + make.bottom.equalToSuperview().inset(5) + } + } + + func configure(information: GameDetail) { + genreInformationView.informationLabel.text = information.genre + developerInformationView.informationLabel.text = information.developer + releaseDateInformationView.informationLabel.text = information.releaseDate + publisherInformationView.informationLabel.text = information.publisher + } + +} From a71a621f0b6a859c756adc7bcb5fd5334fbdc121 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 13 Jul 2023 18:20:05 +0900 Subject: [PATCH 05/17] =?UTF-8?q?refactor:=20detailCollectionView=EA=B0=80?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 코드에서는 추가가 되지 않은 상황 - dataSource 소유를 명시 --- GameScope/GameScope/Controller/DetailViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/GameScope/GameScope/Controller/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift index 2da0372..7ff6558 100644 --- a/GameScope/GameScope/Controller/DetailViewController.swift +++ b/GameScope/GameScope/Controller/DetailViewController.swift @@ -24,10 +24,9 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { frame: view.bounds, collectionViewLayout: generateLayout()) - view.addSubview(collectionView) - collectionView.backgroundColor = .systemBackground collectionView.delegate = self + collectionView.dataSource = self collectionView.register( HeaderView.self, forSupplementaryViewOfKind: DetailViewController.headerElementKind, @@ -46,6 +45,8 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { override func viewDidLoad() { super.viewDidLoad() navigationItem.title = detail?.title + + view.addSubview(detailCollectionView) } } From 16544d96736aee572202d425f1e17ae6f25902a6 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 13 Jul 2023 18:21:42 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20detailCollectionView=EC=97=90=20G?= =?UTF-8?q?ameDetailInformationCell=EC=9D=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게임 정보를 기반으로 Information에 해당하는 Section을 표현하도록 구현 --- .../Controller/DetailViewController.swift | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/GameScope/GameScope/Controller/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift index 7ff6558..14f50c6 100644 --- a/GameScope/GameScope/Controller/DetailViewController.swift +++ b/GameScope/GameScope/Controller/DetailViewController.swift @@ -31,7 +31,9 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { HeaderView.self, forSupplementaryViewOfKind: DetailViewController.headerElementKind, withReuseIdentifier: HeaderView.reuseIdentifier) - + collectionView.register( + GameDetailInformationCell.self, + forCellWithReuseIdentifier: GameDetailInformationCell.reuseIdentifier) return collectionView }() @@ -62,7 +64,7 @@ extension DetailViewController { case .about: return nil case .information: - return nil + return self.generateInformationLayout() case .screenshots: return nil } @@ -70,6 +72,31 @@ extension DetailViewController { return layout } + private func generateInformationLayout() -> NSCollectionLayoutSection { + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(44)) + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: DetailViewController.headerElementKind, + alignment: .top) + + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.9), + heightDimension: .fractionalWidth(0.4)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 1) + + let section = NSCollectionLayoutSection(group: group) + section.boundarySupplementaryItems = [sectionHeader] + section.orthogonalScrollingBehavior = .groupPagingCentered + + return section + } } extension DetailViewController: UICollectionViewDataSource { @@ -80,6 +107,8 @@ extension DetailViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { switch Section.allCases[section] { + case .information: + return 1 default: return 0 } @@ -87,9 +116,27 @@ extension DetailViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { switch Section.allCases[indexPath.section] { + case .information: + guard let detail = detail, + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: GameDetailInformationCell.reuseIdentifier, + for: indexPath) as? GameDetailInformationCell else { + return UICollectionViewCell() + } + cell.configure(information: detail) + return cell default: return UICollectionViewCell() } } + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + if kind == DetailViewController.headerElementKind, + let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.reuseIdentifier, for: indexPath) as? HeaderView { + view.label.text = Section.allCases[indexPath.section].rawValue + return view + } + + return UICollectionReusableView() + } } From b3a7e52b0a27d79cad35019bbdb398e39814c3b7 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 13 Jul 2023 18:22:26 +0900 Subject: [PATCH 07/17] =?UTF-8?q?refactor:=20Header=EC=9D=98=20=EA=B8=80?= =?UTF-8?q?=EC=94=A8=EA=B0=80=20=EC=A7=A4=EB=A6=AC=EB=8A=94=20=EA=B1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GameScope/GameScope/View/HeaderView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GameScope/GameScope/View/HeaderView.swift b/GameScope/GameScope/View/HeaderView.swift index 8453a06..109a9e7 100644 --- a/GameScope/GameScope/View/HeaderView.swift +++ b/GameScope/GameScope/View/HeaderView.swift @@ -16,7 +16,8 @@ class HeaderView: UICollectionReusableView { addSubview(label) label.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(20) + make.leading.trailing.equalToSuperview().inset(20) + make.top.bottom.equalToSuperview() } label.font = .preferredFont(forTextStyle: .title2) From 4961b606b6aea72e8666d87d570ceb9675e83bf8 Mon Sep 17 00:00:00 2001 From: ohdair Date: Sun, 16 Jul 2023 09:26:31 +0900 Subject: [PATCH 08/17] =?UTF-8?q?refactor:=20=EB=B6=88=ED=99=95=EC=8B=A4?= =?UTF-8?q?=ED=95=9C=20=EB=B3=80=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GameScope/GameScope/Controller/DetailViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GameScope/GameScope/Controller/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift index 14f50c6..94a1cc9 100644 --- a/GameScope/GameScope/Controller/DetailViewController.swift +++ b/GameScope/GameScope/Controller/DetailViewController.swift @@ -132,9 +132,9 @@ extension DetailViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { if kind == DetailViewController.headerElementKind, - let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.reuseIdentifier, for: indexPath) as? HeaderView { - view.label.text = Section.allCases[indexPath.section].rawValue - return view + let HeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.reuseIdentifier, for: indexPath) as? HeaderView { + HeaderView.label.text = Section.allCases[indexPath.section].rawValue + return HeaderView } return UICollectionReusableView() From 4fea420eed8131f77f8ee901e237a3fb6021dc32 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 20 Jul 2023 18:52:33 +0900 Subject: [PATCH 09/17] =?UTF-8?q?refactor:=20boundarySupplementaryHeader?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/DetailViewController.swift | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/GameScope/GameScope/Controller/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift index 94a1cc9..e0d8de3 100644 --- a/GameScope/GameScope/Controller/DetailViewController.swift +++ b/GameScope/GameScope/Controller/DetailViewController.swift @@ -36,6 +36,16 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { forCellWithReuseIdentifier: GameDetailInformationCell.reuseIdentifier) return collectionView }() + private let boundarySupplementaryHeader = { + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(44)) + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: DetailViewController.headerElementKind, + alignment: .top) + return sectionHeader + }() private var detail: GameDetail? @@ -73,14 +83,6 @@ extension DetailViewController { } private func generateInformationLayout() -> NSCollectionLayoutSection { - let headerSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1.0), - heightDimension: .estimated(44)) - let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( - layoutSize: headerSize, - elementKind: DetailViewController.headerElementKind, - alignment: .top) - let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) @@ -89,10 +91,12 @@ extension DetailViewController { let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalWidth(0.4)) - let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 1) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, + subitem: item, + count: 1) let section = NSCollectionLayoutSection(group: group) - section.boundarySupplementaryItems = [sectionHeader] + section.boundarySupplementaryItems = [boundarySupplementaryHeader] section.orthogonalScrollingBehavior = .groupPagingCentered return section From 058e2016b3d6cb11a41eec4b021549944bffc06c Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 20 Jul 2023 18:53:32 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20generateAboutLayout=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/DetailViewController.swift | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/GameScope/GameScope/Controller/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift index e0d8de3..e9072bb 100644 --- a/GameScope/GameScope/Controller/DetailViewController.swift +++ b/GameScope/GameScope/Controller/DetailViewController.swift @@ -72,7 +72,7 @@ extension DetailViewController { case .thumbnail: return nil case .about: - return nil + return self.generateAboutLayout() case .information: return self.generateInformationLayout() case .screenshots: @@ -82,6 +82,26 @@ extension DetailViewController { return layout } + private func generateAboutLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(100)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.9), + heightDimension: .estimated(100)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, + subitem: item, + count: 1) + + let section = NSCollectionLayoutSection(group: group) + section.boundarySupplementaryItems = [boundarySupplementaryHeader] + section.orthogonalScrollingBehavior = .groupPagingCentered + + return section + } + private func generateInformationLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), From 181430f29d873e7a8bc6d3878ee97f41564429e4 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 20 Jul 2023 18:54:00 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20GameDetailDescriptionCell=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GameScope/GameScope.xcodeproj/project.pbxproj | 4 + .../View/GameDetailDescriptionCell.swift | 99 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 GameScope/GameScope/View/GameDetailDescriptionCell.swift diff --git a/GameScope/GameScope.xcodeproj/project.pbxproj b/GameScope/GameScope.xcodeproj/project.pbxproj index 67236e0..1faea39 100644 --- a/GameScope/GameScope.xcodeproj/project.pbxproj +++ b/GameScope/GameScope.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ F6B45FDF2A57A10D00F1D0DB /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */; }; F6B45FEA2A5F962200F1D0DB /* GameDetailInformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */; }; F6B45FEC2A5F97DF00F1D0DB /* InformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */; }; + F6B45FEE2A63711F00F1D0DB /* GameDetailDescriptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FED2A63711F00F1D0DB /* GameDetailDescriptionCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -78,6 +79,7 @@ F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailInformationCell.swift; sourceTree = ""; }; F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationView.swift; sourceTree = ""; }; + F6B45FED2A63711F00F1D0DB /* GameDetailDescriptionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailDescriptionCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -106,6 +108,7 @@ F15550712A4EDD0400832E92 /* gameListCollectionViewCell.swift */, F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */, F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */, + F6B45FED2A63711F00F1D0DB /* GameDetailDescriptionCell.swift */, ); path = View; sourceTree = ""; @@ -310,6 +313,7 @@ F6B45FEC2A5F97DF00F1D0DB /* InformationView.swift in Sources */, F6B45FB22A4E775100F1D0DB /* ScreenshotDTO.swift in Sources */, F6B45FDF2A57A10D00F1D0DB /* HeaderView.swift in Sources */, + F6B45FEE2A63711F00F1D0DB /* GameDetailDescriptionCell.swift in Sources */, F6B45FD82A500F5200F1D0DB /* DetailViewController.swift in Sources */, F6B45FB92A4EA9E700F1D0DB /* GameList.swift in Sources */, F6B45FAC2A4E71DA00F1D0DB /* GameListDTO.swift in Sources */, diff --git a/GameScope/GameScope/View/GameDetailDescriptionCell.swift b/GameScope/GameScope/View/GameDetailDescriptionCell.swift new file mode 100644 index 0000000..a47d495 --- /dev/null +++ b/GameScope/GameScope/View/GameDetailDescriptionCell.swift @@ -0,0 +1,99 @@ +// +// GameDetailDescriptionCell.swift +// GameScope +// +// Created by 박재우 on 2023/07/16. +// + +import UIKit + +class GameDetailDescriptionCell: UICollectionViewCell { + + static let reuseIdentifier = "GameDetailDescriptionCell" + + private let descriptionLabel = { + let label = UILabel() + label.numberOfLines = 3 + label.sizeToFit() + return label + }() + private let readMoreButton = { + let button = UIButton() + button.isHidden = true + button.setTitle("+ Read More", for: .normal) + button.setTitleColor(#colorLiteral(red: 0.2980392157, green: 0.8196078431, blue: 0.6980392157, alpha: 1), for: .normal) + return button + }() + private let descriptionStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = 5 + stackView.distribution = .fill + return stackView + }() + private var isHiddenReadMoreButton = false { + willSet { + readMoreButton.isHidden = newValue + } + } + + weak var delegate: GameDetailDescriptionCellDelegate? + + override func layoutSubviews() { + setupHiddenReadMoreButtonIfNeeded() + } + + func configure(description: String) { + addSubview(descriptionStackView) + descriptionStackView.addArrangedSubview(descriptionLabel) + descriptionStackView.addArrangedSubview(readMoreButton) + descriptionStackView.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + } + + descriptionLabel.text = description + readMoreButton.addTarget(self, + action: #selector(tappedReadMoreButton), + for: .touchDown) + } + + @objc private func tappedReadMoreButton(_ sender: UIButton) { + delegate?.gameDetailDescriptionCell(self, didButtonTapped: sender) + } + + private func setupHiddenReadMoreButtonIfNeeded() { + guard isHiddenReadMoreButton == false, + descriptionLabel.maxNumberOfLines > 3 else { + return + } + + readMoreButton.isHidden = false + } + + func expandDetailDescription() { + isHiddenReadMoreButton = true + descriptionLabel.numberOfLines = 0 + } +} + +private extension UILabel { + var maxNumberOfLines: Int { + let maxSize = CGSize(width: frame.size.width, height: .infinity) + let text = (self.text ?? "") as NSString + let textHeight = text.boundingRect(with: maxSize, + options: .usesLineFragmentOrigin, + attributes: [.font: font as Any], + context: nil).height + let lineHeight = font.lineHeight + return Int(ceil(textHeight / lineHeight)) + } +} + +protocol GameDetailDescriptionCellDelegate: NSObject { + func gameDetailDescriptionCell( + _ gameDetailDescriptionCell: GameDetailDescriptionCell, + didButtonTapped sender: UIButton + ) +} From 6c039d14a049efa0a7d46704150e5f811ff4fd83 Mon Sep 17 00:00:00 2001 From: ohdair Date: Thu, 20 Jul 2023 18:54:39 +0900 Subject: [PATCH 12/17] =?UTF-8?q?feat:=20DetailViewController=EC=97=90=20A?= =?UTF-8?q?bout=20=EA=B4=80=EB=A0=A8=20Cell=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/DetailViewController.swift | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/GameScope/GameScope/Controller/DetailViewController.swift b/GameScope/GameScope/Controller/DetailViewController.swift index e9072bb..e103006 100644 --- a/GameScope/GameScope/Controller/DetailViewController.swift +++ b/GameScope/GameScope/Controller/DetailViewController.swift @@ -34,6 +34,9 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { collectionView.register( GameDetailInformationCell.self, forCellWithReuseIdentifier: GameDetailInformationCell.reuseIdentifier) + collectionView.register( + GameDetailDescriptionCell.self, + forCellWithReuseIdentifier: GameDetailDescriptionCell.reuseIdentifier) return collectionView }() private let boundarySupplementaryHeader = { @@ -131,7 +134,7 @@ extension DetailViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { switch Section.allCases[section] { - case .information: + case .about, .information: return 1 default: return 0 @@ -140,6 +143,16 @@ extension DetailViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { switch Section.allCases[indexPath.section] { + case .about: + guard let detail, + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: GameDetailDescriptionCell.reuseIdentifier, + for: indexPath) as? GameDetailDescriptionCell else { + return UICollectionViewCell() + } + cell.configure(description: detail.description) + cell.delegate = self + return cell case .information: guard let detail = detail, let cell = collectionView.dequeueReusableCell( @@ -163,4 +176,15 @@ extension DetailViewController: UICollectionViewDataSource { return UICollectionReusableView() } + +} + +extension DetailViewController: GameDetailDescriptionCellDelegate { + func gameDetailDescriptionCell( + _ gameDetailDescriptionCell: GameDetailDescriptionCell, + didButtonTapped sender: UIButton + ) { + gameDetailDescriptionCell.expandDetailDescription() + self.detailCollectionView.reloadData() + } } From 6b2c4c07766cbb299755dddb6d6bd2a732061362 Mon Sep 17 00:00:00 2001 From: ohdair Date: Mon, 24 Jul 2023 09:57:53 +0900 Subject: [PATCH 13/17] =?UTF-8?q?file:=20merge=EB=90=98=EB=A9=B4=EC=84=9C?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=EB=90=9C=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GameScope/GameScope.xcodeproj/project.pbxproj | 16 +- .../Controllers/DetailViewController.swift | 190 ++++++++++++++++++ 2 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 GameScope/GameScope/Controllers/DetailViewController.swift diff --git a/GameScope/GameScope.xcodeproj/project.pbxproj b/GameScope/GameScope.xcodeproj/project.pbxproj index db303f9..9d1dfd4 100644 --- a/GameScope/GameScope.xcodeproj/project.pbxproj +++ b/GameScope/GameScope.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ F1E20C7C2A4D6E1C00772B80 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F1E20C7B2A4D6E1C00772B80 /* Assets.xcassets */; }; F1E20C7F2A4D6E1C00772B80 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F1E20C7D2A4D6E1C00772B80 /* LaunchScreen.storyboard */; }; F1E20C8D2A4EACF900772B80 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1E20C8C2A4EACF900772B80 /* SnapKit */; }; + F60B0D062A6E03CA004CE3D5 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60B0D052A6E03CA004CE3D5 /* DetailViewController.swift */; }; F6B45FA82A4DA96000F1D0DB /* popular-pvp-games.json in Resources */ = {isa = PBXBuildFile; fileRef = F6B45FA72A4DA96000F1D0DB /* popular-pvp-games.json */; }; F6B45FAA2A4DA9C200F1D0DB /* detail-game-pubg.json in Resources */ = {isa = PBXBuildFile; fileRef = F6B45FA92A4DA9C200F1D0DB /* detail-game-pubg.json */; }; F6B45FAC2A4E71DA00F1D0DB /* GameListDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FAB2A4E71DA00F1D0DB /* GameListDTO.swift */; }; @@ -42,7 +43,6 @@ F6B45FD32A4EC36C00F1D0DB /* detail-game-pubg.json in Resources */ = {isa = PBXBuildFile; fileRef = F6B45FA92A4DA9C200F1D0DB /* detail-game-pubg.json */; }; F6B45FD52A4EC5D900F1D0DB /* GameDetailDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FD42A4EC5D900F1D0DB /* GameDetailDTOTests.swift */; }; F6B45FD62A4EC6A000F1D0DB /* wrong-game-pubg.json in Resources */ = {isa = PBXBuildFile; fileRef = F6B45FD22A4EBF4A00F1D0DB /* wrong-game-pubg.json */; }; - F6B45FD82A500F5200F1D0DB /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FD72A500F5200F1D0DB /* DetailViewController.swift */; }; F6B45FDF2A57A10D00F1D0DB /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */; }; F6B45FEA2A5F962200F1D0DB /* GameDetailInformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */; }; F6B45FEC2A5F97DF00F1D0DB /* InformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */; }; @@ -76,6 +76,7 @@ F1E20C7B2A4D6E1C00772B80 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F1E20C7E2A4D6E1C00772B80 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F1E20C802A4D6E1C00772B80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F60B0D052A6E03CA004CE3D5 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; F6B45FA72A4DA96000F1D0DB /* popular-pvp-games.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "popular-pvp-games.json"; sourceTree = ""; }; F6B45FA92A4DA9C200F1D0DB /* detail-game-pubg.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "detail-game-pubg.json"; sourceTree = ""; }; F6B45FAB2A4E71DA00F1D0DB /* GameListDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameListDTO.swift; sourceTree = ""; }; @@ -89,7 +90,6 @@ F6B45FD02A4EBEEA00F1D0DB /* wrong-pvp-games.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "wrong-pvp-games.json"; sourceTree = ""; }; F6B45FD22A4EBF4A00F1D0DB /* wrong-game-pubg.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "wrong-game-pubg.json"; sourceTree = ""; }; F6B45FD42A4EC5D900F1D0DB /* GameDetailDTOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailDTOTests.swift; sourceTree = ""; }; - F6B45FD72A500F5200F1D0DB /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailInformationCell.swift; sourceTree = ""; }; F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationView.swift; sourceTree = ""; }; @@ -119,6 +119,7 @@ isa = PBXGroup; children = ( F14B01752A513FEF00D43B1D /* GameListCollectionViewController.swift */, + F60B0D052A6E03CA004CE3D5 /* DetailViewController.swift */, ); path = Controllers; sourceTree = ""; @@ -153,7 +154,6 @@ isa = PBXGroup; children = ( F6B45FDE2A57A10D00F1D0DB /* HeaderView.swift */, - F15550712A4EDD0400832E92 /* gameListCollectionViewCell.swift */, F6B45FE92A5F962200F1D0DB /* GameDetailInformationCell.swift */, F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */, F6B45FED2A63711F00F1D0DB /* GameDetailDescriptionCell.swift */, @@ -168,6 +168,7 @@ F1E20C712A4D6E1B00772B80 /* GameScope */, F6B45FC12A4EB9CC00F1D0DB /* GameScopeTests */, F1E20C702A4D6E1B00772B80 /* Products */, + F60B0D042A6E036E004CE3D5 /* Recovered References */, ); sourceTree = ""; }; @@ -203,6 +204,13 @@ path = GameScope; sourceTree = ""; }; + F60B0D042A6E036E004CE3D5 /* Recovered References */ = { + isa = PBXGroup; + children = ( + ); + name = "Recovered References"; + sourceTree = ""; + }; F6B45FB42A4E7A1800F1D0DB /* DTO */ = { isa = PBXGroup; children = ( @@ -356,6 +364,7 @@ F1E20C732A4D6E1B00772B80 /* AppDelegate.swift in Sources */, F14B017B2A5195DD00D43B1D /* GameManager.swift in Sources */, F14B01762A513FEF00D43B1D /* GameListCollectionViewController.swift in Sources */, + F60B0D062A6E03CA004CE3D5 /* DetailViewController.swift in Sources */, F6B45FAE2A4E752900F1D0DB /* GameDetailDTO.swift in Sources */, F6B45FB02A4E768B00F1D0DB /* MinimumSystemRequirementsDTO.swift in Sources */, F14B01952A56655000D43B1D /* URLResponse.swift in Sources */, @@ -364,7 +373,6 @@ F6B45FB22A4E775100F1D0DB /* ScreenshotDTO.swift in Sources */, F6B45FDF2A57A10D00F1D0DB /* HeaderView.swift in Sources */, F6B45FEE2A63711F00F1D0DB /* GameDetailDescriptionCell.swift in Sources */, - F6B45FD82A500F5200F1D0DB /* DetailViewController.swift in Sources */, F6B45FB92A4EA9E700F1D0DB /* GameList.swift in Sources */, F14B017D2A52AF2B00D43B1D /* JSONDeserializer.swift in Sources */, F6B45FAC2A4E71DA00F1D0DB /* GameListDTO.swift in Sources */, diff --git a/GameScope/GameScope/Controllers/DetailViewController.swift b/GameScope/GameScope/Controllers/DetailViewController.swift new file mode 100644 index 0000000..e103006 --- /dev/null +++ b/GameScope/GameScope/Controllers/DetailViewController.swift @@ -0,0 +1,190 @@ +// +// DetailViewController.swift +// GameScope +// +// Created by 박재우 on 2023/07/01. +// + +import UIKit +import SnapKit + +class DetailViewController: UIViewController, UICollectionViewDelegate { + + static let headerElementKind = "header-element-kind" + + enum Section: String, CaseIterable { + case thumbnail + case about = "About" + case information = "Information" + case screenshots = "ScreenShots" + } + + private lazy var detailCollectionView = { + let collectionView = UICollectionView( + frame: view.bounds, + collectionViewLayout: generateLayout()) + + collectionView.backgroundColor = .systemBackground + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register( + HeaderView.self, + forSupplementaryViewOfKind: DetailViewController.headerElementKind, + withReuseIdentifier: HeaderView.reuseIdentifier) + collectionView.register( + GameDetailInformationCell.self, + forCellWithReuseIdentifier: GameDetailInformationCell.reuseIdentifier) + collectionView.register( + GameDetailDescriptionCell.self, + forCellWithReuseIdentifier: GameDetailDescriptionCell.reuseIdentifier) + return collectionView + }() + private let boundarySupplementaryHeader = { + let headerSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(44)) + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: DetailViewController.headerElementKind, + alignment: .top) + return sectionHeader + }() + + private var detail: GameDetail? + + convenience init(gameDetail: GameDetail) { + self.init() + detail = gameDetail + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = detail?.title + + view.addSubview(detailCollectionView) + } + +} + +extension DetailViewController { + + private func generateLayout() -> UICollectionViewLayout { + let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in + let sectionLayoutKind = Section.allCases[sectionIndex] + switch sectionLayoutKind { + case .thumbnail: + return nil + case .about: + return self.generateAboutLayout() + case .information: + return self.generateInformationLayout() + case .screenshots: + return nil + } + } + return layout + } + + private func generateAboutLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(100)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.9), + heightDimension: .estimated(100)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, + subitem: item, + count: 1) + + let section = NSCollectionLayoutSection(group: group) + section.boundarySupplementaryItems = [boundarySupplementaryHeader] + section.orthogonalScrollingBehavior = .groupPagingCentered + + return section + } + + private func generateInformationLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.9), + heightDimension: .fractionalWidth(0.4)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, + subitem: item, + count: 1) + + let section = NSCollectionLayoutSection(group: group) + section.boundarySupplementaryItems = [boundarySupplementaryHeader] + section.orthogonalScrollingBehavior = .groupPagingCentered + + return section + } +} + +extension DetailViewController: UICollectionViewDataSource { + + func numberOfSections(in collectionView: UICollectionView) -> Int { + Section.allCases.count + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + switch Section.allCases[section] { + case .about, .information: + return 1 + default: + return 0 + } + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + switch Section.allCases[indexPath.section] { + case .about: + guard let detail, + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: GameDetailDescriptionCell.reuseIdentifier, + for: indexPath) as? GameDetailDescriptionCell else { + return UICollectionViewCell() + } + cell.configure(description: detail.description) + cell.delegate = self + return cell + case .information: + guard let detail = detail, + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: GameDetailInformationCell.reuseIdentifier, + for: indexPath) as? GameDetailInformationCell else { + return UICollectionViewCell() + } + cell.configure(information: detail) + return cell + default: + return UICollectionViewCell() + } + } + + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + if kind == DetailViewController.headerElementKind, + let HeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.reuseIdentifier, for: indexPath) as? HeaderView { + HeaderView.label.text = Section.allCases[indexPath.section].rawValue + return HeaderView + } + + return UICollectionReusableView() + } + +} + +extension DetailViewController: GameDetailDescriptionCellDelegate { + func gameDetailDescriptionCell( + _ gameDetailDescriptionCell: GameDetailDescriptionCell, + didButtonTapped sender: UIButton + ) { + gameDetailDescriptionCell.expandDetailDescription() + self.detailCollectionView.reloadData() + } +} From 543a6dee2f4e2adca0324c36068bff83fd96e772 Mon Sep 17 00:00:00 2001 From: ohdair Date: Mon, 24 Jul 2023 15:33:45 +0900 Subject: [PATCH 14/17] =?UTF-8?q?feat:=20#12=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EB=84=A3=EB=8A=94=20Cel?= =?UTF-8?q?l=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GameScope/GameScope.xcodeproj/project.pbxproj | 4 ++ .../GameScope/View/GameDetailImageCell.swift | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 GameScope/GameScope/View/GameDetailImageCell.swift diff --git a/GameScope/GameScope.xcodeproj/project.pbxproj b/GameScope/GameScope.xcodeproj/project.pbxproj index 9664cd0..3622f74 100644 --- a/GameScope/GameScope.xcodeproj/project.pbxproj +++ b/GameScope/GameScope.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ F1E20C7F2A4D6E1C00772B80 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F1E20C7D2A4D6E1C00772B80 /* LaunchScreen.storyboard */; }; F1E20C8D2A4EACF900772B80 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1E20C8C2A4EACF900772B80 /* SnapKit */; }; F60B0D062A6E03CA004CE3D5 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60B0D052A6E03CA004CE3D5 /* DetailViewController.swift */; }; + F60B0D082A6E42C5004CE3D5 /* GameDetailImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60B0D072A6E42C5004CE3D5 /* GameDetailImageCell.swift */; }; F6B45FA82A4DA96000F1D0DB /* popular-pvp-games.json in Resources */ = {isa = PBXBuildFile; fileRef = F6B45FA72A4DA96000F1D0DB /* popular-pvp-games.json */; }; F6B45FAA2A4DA9C200F1D0DB /* detail-game-pubg.json in Resources */ = {isa = PBXBuildFile; fileRef = F6B45FA92A4DA9C200F1D0DB /* detail-game-pubg.json */; }; F6B45FAC2A4E71DA00F1D0DB /* GameListDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B45FAB2A4E71DA00F1D0DB /* GameListDTO.swift */; }; @@ -79,6 +80,7 @@ F1E20C7E2A4D6E1C00772B80 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F1E20C802A4D6E1C00772B80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F60B0D052A6E03CA004CE3D5 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; + F60B0D072A6E42C5004CE3D5 /* GameDetailImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailImageCell.swift; sourceTree = ""; }; F6B45FA72A4DA96000F1D0DB /* popular-pvp-games.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "popular-pvp-games.json"; sourceTree = ""; }; F6B45FA92A4DA9C200F1D0DB /* detail-game-pubg.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "detail-game-pubg.json"; sourceTree = ""; }; F6B45FAB2A4E71DA00F1D0DB /* GameListDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameListDTO.swift; sourceTree = ""; }; @@ -161,6 +163,7 @@ F6B45FEB2A5F97DF00F1D0DB /* InformationView.swift */, F6B45FED2A63711F00F1D0DB /* GameDetailDescriptionCell.swift */, F15550712A4EDD0400832E92 /* GameListCollectionViewCell.swift */, + F60B0D072A6E42C5004CE3D5 /* GameDetailImageCell.swift */, ); path = View; sourceTree = ""; @@ -377,6 +380,7 @@ F6B45FB22A4E775100F1D0DB /* ScreenshotDTO.swift in Sources */, F6B45FDF2A57A10D00F1D0DB /* HeaderView.swift in Sources */, F6B45FEE2A63711F00F1D0DB /* GameDetailDescriptionCell.swift in Sources */, + F60B0D082A6E42C5004CE3D5 /* GameDetailImageCell.swift in Sources */, F6B45FB92A4EA9E700F1D0DB /* GameList.swift in Sources */, F14B017D2A52AF2B00D43B1D /* JSONDeserializer.swift in Sources */, F6B45FAC2A4E71DA00F1D0DB /* GameListDTO.swift in Sources */, diff --git a/GameScope/GameScope/View/GameDetailImageCell.swift b/GameScope/GameScope/View/GameDetailImageCell.swift new file mode 100644 index 0000000..07160d6 --- /dev/null +++ b/GameScope/GameScope/View/GameDetailImageCell.swift @@ -0,0 +1,47 @@ +// +// GameDetailImageCell.swift +// GameScope +// +// Created by 박재우 on 2023/07/24. +// + +import UIKit + +class GameDetailImageCell: UICollectionViewCell { + + static let reuseIdentifier = "GameDetailImageCell" + + private let imageView = { + let imageView = UIImageView() + imageView.backgroundColor = #colorLiteral(red: 0.7496843338, green: 0.7496843338, blue: 0.7496843338, alpha: 1) + imageView.contentMode = .scaleToFill + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + layer.cornerRadius = 20 + layer.masksToBounds = true + + addSubview(imageView) + + imageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + func configure(urlString: String) { + Task { + let image = try await GameManager().dispatchImage(of: urlString) + imageView.image = image + } + } +} From 7f80bac18a8dd92cc5df857b5391619f7edac22e Mon Sep 17 00:00:00 2001 From: ohdair Date: Mon, 24 Jul 2023 15:41:49 +0900 Subject: [PATCH 15/17] =?UTF-8?q?feat:=20DetailViewController=20image=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20layout=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generateImageLayout 메서드 추가 - GameDetailImageCell 등록 및 구성하도록 추가 --- .../Controllers/DetailViewController.swift | 73 +++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/GameScope/GameScope/Controllers/DetailViewController.swift b/GameScope/GameScope/Controllers/DetailViewController.swift index e103006..7f11ea5 100644 --- a/GameScope/GameScope/Controllers/DetailViewController.swift +++ b/GameScope/GameScope/Controllers/DetailViewController.swift @@ -37,6 +37,9 @@ class DetailViewController: UIViewController, UICollectionViewDelegate { collectionView.register( GameDetailDescriptionCell.self, forCellWithReuseIdentifier: GameDetailDescriptionCell.reuseIdentifier) + collectionView.register( + GameDetailImageCell.self, + forCellWithReuseIdentifier: GameDetailImageCell.reuseIdentifier) return collectionView }() private let boundarySupplementaryHeader = { @@ -72,14 +75,12 @@ extension DetailViewController { let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in let sectionLayoutKind = Section.allCases[sectionIndex] switch sectionLayoutKind { - case .thumbnail: - return nil + case .thumbnail, .screenshots: + return self.generateImageLayout() case .about: return self.generateAboutLayout() case .information: return self.generateInformationLayout() - case .screenshots: - return nil } } return layout @@ -124,6 +125,27 @@ extension DetailViewController { return section } + + private func generateImageLayout() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.9), + heightDimension: .fractionalWidth(0.6)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, + subitem: item, + count: 1) + group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5) + + let section = NSCollectionLayoutSection(group: group) + section.boundarySupplementaryItems = [boundarySupplementaryHeader] + section.orthogonalScrollingBehavior = .groupPagingCentered + + return section + } } extension DetailViewController: UICollectionViewDataSource { @@ -133,19 +155,34 @@ extension DetailViewController: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + guard let detail else { + return 0 + } + switch Section.allCases[section] { - case .about, .information: + case .thumbnail, .about, .information: return 1 - default: - return 0 + case .screenshots: + return detail.screenshots.count } } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let detail else { + return UICollectionViewCell() + } + switch Section.allCases[indexPath.section] { + case .thumbnail: + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: GameDetailImageCell.reuseIdentifier, + for: indexPath) as? GameDetailImageCell else { + return UICollectionViewCell() + } + cell.configure(urlString: detail.thumbnail) + return cell case .about: - guard let detail, - let cell = collectionView.dequeueReusableCell( + guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: GameDetailDescriptionCell.reuseIdentifier, for: indexPath) as? GameDetailDescriptionCell else { return UICollectionViewCell() @@ -154,22 +191,30 @@ extension DetailViewController: UICollectionViewDataSource { cell.delegate = self return cell case .information: - guard let detail = detail, - let cell = collectionView.dequeueReusableCell( + guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: GameDetailInformationCell.reuseIdentifier, for: indexPath) as? GameDetailInformationCell else { return UICollectionViewCell() } cell.configure(information: detail) return cell - default: - return UICollectionViewCell() + case .screenshots: + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: GameDetailImageCell.reuseIdentifier, + for: indexPath) as? GameDetailImageCell else { + return UICollectionViewCell() + } + cell.configure(urlString: detail.screenshots[indexPath.row]) + return cell } } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { if kind == DetailViewController.headerElementKind, - let HeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.reuseIdentifier, for: indexPath) as? HeaderView { + let HeaderView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: HeaderView.reuseIdentifier, + for: indexPath) as? HeaderView { HeaderView.label.text = Section.allCases[indexPath.section].rawValue return HeaderView } From f742b865f5cb6438e952d27198227cf813b96c20 Mon Sep 17 00:00:00 2001 From: ohdair Date: Mon, 24 Jul 2023 16:05:49 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20GameManager=20dispatchImage=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문자열로 이뤄진 url을 통해 Image를 받는 dispatchImage 추가 --- GameScope/GameScope/Util/GameManager.swift | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/GameScope/GameScope/Util/GameManager.swift b/GameScope/GameScope/Util/GameManager.swift index 4205ab2..34d1dd6 100644 --- a/GameScope/GameScope/Util/GameManager.swift +++ b/GameScope/GameScope/Util/GameManager.swift @@ -78,6 +78,28 @@ final class GameManager { } } + func dispatchImage(of url: String) async throws -> UIImage? { + let cacheKey = NSString(string: url) + if let chachedImage = ImageCacheManager.shared.object(forKey: cacheKey) { + return chachedImage + } + + guard let thumbnailURL = URL(string: url) else { throw NetworkError.invalidURL } + let urlRequest = URLRequest(url: thumbnailURL) + + let imageResult = try await networkDispatcher.performRequest(urlRequest) + + switch imageResult { + case .success(let data): + guard let thumbnailImage = UIImage(data: data) else { throw NetworkError.emptyData} + ImageCacheManager.shared.setObject(thumbnailImage, forKey: cacheKey) + return thumbnailImage + case .failure(let error): + print(error.errorDescription) + return nil + } + } + // MARK: - Private private func fetchGameList(of listKind: dummyConstants) -> GameListDTO? { guard let dataUrl = Bundle.main.url( From fccba35b0c951e2506112e567a28d6ed0d7bdbda Mon Sep 17 00:00:00 2001 From: ohdair Date: Mon, 24 Jul 2023 16:07:59 +0900 Subject: [PATCH 17/17] =?UTF-8?q?feat:=20GameListCollectionView=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Cell=20=EC=84=A0=ED=83=9D=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cell 선택시 json 파일을 통한 정보를 보여주도록 설정 --- .../GameListCollectionViewController.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/GameScope/GameScope/Controllers/GameListCollectionViewController.swift b/GameScope/GameScope/Controllers/GameListCollectionViewController.swift index 0a1ebf6..2fd5e6d 100644 --- a/GameScope/GameScope/Controllers/GameListCollectionViewController.swift +++ b/GameScope/GameScope/Controllers/GameListCollectionViewController.swift @@ -90,6 +90,7 @@ final class GameListCollectionViewController: UIViewController { collectionView.register( GameListCollectionViewCell.self, forCellWithReuseIdentifier: GameListCollectionViewCell.identifier) + collectionView.delegate = self return collectionView }() @@ -223,3 +224,21 @@ extension GameListCollectionViewController { } } + +extension GameListCollectionViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let url = Bundle.main.url(forResource: "detail-game-pubg", withExtension: "json") else { + return + } + + do { + let data = try String(contentsOf: url).data(using: .utf8)! + let gameDetailDTO = try JSONDecoder().decode(GameDetailDTO.self, from: data) + let result = gameDetailDTO.convert() + let albumDetailVC = DetailViewController(gameDetail: result) + navigationController?.pushViewController(albumDetailVC, animated: true) + } catch { + print(error) + } + } +}