From a5970fc64dd23a733de926823ff34e906a71ec8b Mon Sep 17 00:00:00 2001 From: iOS-Yetti Date: Wed, 16 Aug 2023 14:39:31 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=B3=80=EA=B2=BD=20=EB=B2=84=ED=8A=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 --- .../Controller/BoxOfficeViewController.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/BoxOffice/Controller/BoxOfficeViewController.swift b/BoxOffice/Controller/BoxOfficeViewController.swift index 2f2031ab..6607b804 100644 --- a/BoxOffice/Controller/BoxOfficeViewController.swift +++ b/BoxOffice/Controller/BoxOfficeViewController.swift @@ -12,6 +12,7 @@ final class BoxOfficeViewController: UIViewController, URLSessionDelegate { private var refreshControl = UIRefreshControl() private var dataSource: UICollectionViewDiffableDataSource? private var date: Date = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date() + private var isListMode = true private let collectionView: UICollectionView = { let configuration = UICollectionLayoutListConfiguration(appearance: .plain) @@ -58,11 +59,16 @@ extension BoxOfficeViewController { private func setUpUI() { let safeArea = view.safeAreaLayoutGuide let dateSelectionButton = UIBarButtonItem(title: "날짜선택", style: .plain, target: self, action: #selector(showCalendar)) + let modeChangeButton = UIBarButtonItem(title: "화면 모드 변경", style: .plain, target: self, action: #selector(hitChangeModeButton)) + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) + view.backgroundColor = .systemBackground view.addSubview(collectionView) view.addSubview(indicatorView) + self.navigationController?.isToolbarHidden = false + self.toolbarItems = [flexibleSpace, modeChangeButton, flexibleSpace] self.title = getDateString(format: Namespace.dateWithHyphen) self.navigationItem.rightBarButtonItem = dateSelectionButton @@ -84,6 +90,19 @@ extension BoxOfficeViewController { self.present(viewController, animated: true) } + + @objc func hitChangeModeButton() { + let mode: String = isListMode == true ? "아이콘" : "리스트" + let alert = UIAlertController(title: "화면 모드 변경", message: nil, preferredStyle: .actionSheet) + let modeChangeAction = UIAlertAction(title: mode, style: .default) { _ in + self.isListMode.toggle() + } + let cancelAction = UIAlertAction(title: "취소", style: .cancel) + + alert.addAction(modeChangeAction) + alert.addAction(cancelAction) + present(alert, animated: true) + } } extension BoxOfficeViewController { From 569af41e23450c62b3ddc89f0df2586434e37a23 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Wed, 16 Aug 2023 14:54:19 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20BoxOfficeRankingIconCell=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BoxOffice.xcodeproj/project.pbxproj | 12 +- .../Controller/BoxOfficeViewController.swift | 9 +- BoxOffice/View/BoxOfficeRankingIconCell.swift | 119 ++++++++++++++++++ ...l.swift => BoxOfficeRankingListCell.swift} | 8 +- 4 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 BoxOffice/View/BoxOfficeRankingIconCell.swift rename BoxOffice/View/{BoxOfficeRankingCell.swift => BoxOfficeRankingListCell.swift} (95%) diff --git a/BoxOffice.xcodeproj/project.pbxproj b/BoxOffice.xcodeproj/project.pbxproj index aeb06727..3bf869e6 100644 --- a/BoxOffice.xcodeproj/project.pbxproj +++ b/BoxOffice.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 3B8F5FD42A73556200D1A7F6 /* BoxOfficeNetworkingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8F5FD32A73556200D1A7F6 /* BoxOfficeNetworkingTest.swift */; }; 3BF7147F2A6E75C6006F274D /* BoxOfficeDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF7147E2A6E75C6006F274D /* BoxOfficeDecodingTests.swift */; }; 3BF714862A6E78E3006F274D /* DecodingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF714852A6E78E3006F274D /* DecodingManager.swift */; }; - 3BF7D9AE2A79E69800996A5E /* BoxOfficeRankingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF7D9AD2A79E69800996A5E /* BoxOfficeRankingCell.swift */; }; + 3BF7D9AE2A79E69800996A5E /* BoxOfficeRankingListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF7D9AD2A79E69800996A5E /* BoxOfficeRankingListCell.swift */; }; 3BF7D9B12A7C7D0600996A5E /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF7D9B02A7C7D0600996A5E /* String+.swift */; }; 3BF7D9B62A81C28400996A5E /* DaumImageEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF7D9B52A81C28400996A5E /* DaumImageEntity.swift */; }; 3BF7D9B82A81D69300996A5E /* MovieDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF7D9B72A81D69300996A5E /* MovieDetailViewController.swift */; }; @@ -25,6 +25,7 @@ 63DF20F82970E1A1005DF7D1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */; }; 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */; }; BA1A55AE2A81DF3D0012C89D /* MovieDetailStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1A55AD2A81DF3D0012C89D /* MovieDetailStackView.swift */; }; + BA1A55B82A8C99320012C89D /* BoxOfficeRankingIconCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1A55B72A8C99320012C89D /* BoxOfficeRankingIconCell.swift */; }; BAAC6F2E2A6E6B7700AD34ED /* box_office_sample.json in Resources */ = {isa = PBXBuildFile; fileRef = BAAC6F2D2A6E6B7700AD34ED /* box_office_sample.json */; }; BAAC6F302A6E6C4B00AD34ED /* BoxOfficeEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAC6F2F2A6E6C4B00AD34ED /* BoxOfficeEntity.swift */; }; BAAC6F362A710D8100AD34ED /* MovieDetailEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAC6F352A710D8100AD34ED /* MovieDetailEntity.swift */; }; @@ -53,7 +54,7 @@ 3BF7147C2A6E75C6006F274D /* BoxOfficeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BoxOfficeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3BF7147E2A6E75C6006F274D /* BoxOfficeDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeDecodingTests.swift; sourceTree = ""; }; 3BF714852A6E78E3006F274D /* DecodingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingManager.swift; sourceTree = ""; }; - 3BF7D9AD2A79E69800996A5E /* BoxOfficeRankingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeRankingCell.swift; sourceTree = ""; }; + 3BF7D9AD2A79E69800996A5E /* BoxOfficeRankingListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeRankingListCell.swift; sourceTree = ""; }; 3BF7D9B02A7C7D0600996A5E /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; 3BF7D9B52A81C28400996A5E /* DaumImageEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaumImageEntity.swift; sourceTree = ""; }; 3BF7D9B72A81D69300996A5E /* MovieDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailViewController.swift; sourceTree = ""; }; @@ -66,6 +67,7 @@ 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 = ""; }; BA1A55AD2A81DF3D0012C89D /* MovieDetailStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailStackView.swift; sourceTree = ""; }; + BA1A55B72A8C99320012C89D /* BoxOfficeRankingIconCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeRankingIconCell.swift; sourceTree = ""; }; BAAC6F2D2A6E6B7700AD34ED /* box_office_sample.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = box_office_sample.json; sourceTree = ""; }; BAAC6F2F2A6E6C4B00AD34ED /* BoxOfficeEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeEntity.swift; sourceTree = ""; }; BAAC6F352A710D8100AD34ED /* MovieDetailEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailEntity.swift; sourceTree = ""; }; @@ -133,7 +135,8 @@ isa = PBXGroup; children = ( 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */, - 3BF7D9AD2A79E69800996A5E /* BoxOfficeRankingCell.swift */, + 3BF7D9AD2A79E69800996A5E /* BoxOfficeRankingListCell.swift */, + BA1A55B72A8C99320012C89D /* BoxOfficeRankingIconCell.swift */, BA1A55AD2A81DF3D0012C89D /* MovieDetailStackView.swift */, ); path = View; @@ -303,6 +306,7 @@ buildActionMask = 2147483647; files = ( BAAC6F4A2A734D1B00AD34ED /* URLSessionProtocol.swift in Sources */, + BA1A55B82A8C99320012C89D /* BoxOfficeRankingIconCell.swift in Sources */, 3BF7D9B82A81D69300996A5E /* MovieDetailViewController.swift in Sources */, 3BF7D9BC2A85DFCE00996A5E /* CalendarViewController.swift in Sources */, 63DF20F32970E1A0005DF7D1 /* BoxOfficeViewController.swift in Sources */, @@ -316,7 +320,7 @@ 3BF714862A6E78E3006F274D /* DecodingManager.swift in Sources */, 3B8F5F9E2A72186D00D1A7F6 /* NetworkConfiguration.swift in Sources */, 3B8F5F9C2A71032C00D1A7F6 /* Error.swift in Sources */, - 3BF7D9AE2A79E69800996A5E /* BoxOfficeRankingCell.swift in Sources */, + 3BF7D9AE2A79E69800996A5E /* BoxOfficeRankingListCell.swift in Sources */, 3BF7D9B12A7C7D0600996A5E /* String+.swift in Sources */, 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */, ); diff --git a/BoxOffice/Controller/BoxOfficeViewController.swift b/BoxOffice/Controller/BoxOfficeViewController.swift index 6607b804..6dee4ff3 100644 --- a/BoxOffice/Controller/BoxOfficeViewController.swift +++ b/BoxOffice/Controller/BoxOfficeViewController.swift @@ -19,7 +19,8 @@ final class BoxOfficeViewController: UIViewController, URLSessionDelegate { let layout = UICollectionViewCompositionalLayout.list(using: configuration) let view = UICollectionView(frame: .zero, collectionViewLayout: layout) view.translatesAutoresizingMaskIntoConstraints = false - view.register(BoxOfficeRankingCell.self, forCellWithReuseIdentifier: BoxOfficeRankingCell.cellIdentifier) + view.register(BoxOfficeRankingListCell.self, forCellWithReuseIdentifier: BoxOfficeRankingListCell.cellIdentifier) + view.register(BoxOfficeRankingIconCell.self, forCellWithReuseIdentifier: BoxOfficeRankingIconCell.cellIdentifier) return view }() @@ -124,12 +125,12 @@ extension BoxOfficeViewController { private func setUpDataSource() { dataSource = UICollectionViewDiffableDataSource(collectionView: self.collectionView) { (collectionView, indexPath, data) -> UICollectionViewCell? in - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeRankingCell.cellIdentifier, for: indexPath) as? BoxOfficeRankingCell else { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeRankingListCell.cellIdentifier, for: indexPath) as? BoxOfficeRankingListCell else { return UICollectionViewCell() } - + cell.setUpLabelText(data) - + return cell } } diff --git a/BoxOffice/View/BoxOfficeRankingIconCell.swift b/BoxOffice/View/BoxOfficeRankingIconCell.swift new file mode 100644 index 00000000..e672307d --- /dev/null +++ b/BoxOffice/View/BoxOfficeRankingIconCell.swift @@ -0,0 +1,119 @@ +// +// BoxOfficeRankingIconCell.swift +// BoxOffice +// +// Created by Min Hyun on 2023/08/16. +// + +import UIKit + +class BoxOfficeRankingIconCell: UICollectionViewCell { + static let cellIdentifier = "BoxOfficeIconCell" + + private let rankLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.font = .preferredFont(forTextStyle: .largeTitle) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + private let rankIntensityLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.font = .preferredFont(forTextStyle: .callout) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + private let movieNameLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .title3) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + private let audienceLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.alignment = .center + stackView.distribution = .equalSpacing + + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setUpUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUpUI() { + self.layer.borderWidth = 2.0 + self.layer.borderColor = .init(gray: 0.5, alpha: 1.0) + + contentView.addSubview(stackView) + stackView.addArrangedSubview(rankLabel) + stackView.addArrangedSubview(movieNameLabel) + stackView.addArrangedSubview(rankIntensityLabel) + stackView.addArrangedSubview(audienceLabel) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.9), + stackView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.9) + ]) + } + + func setUpLabelText(_ data: BoxOfficeEntity.BoxOfficeResult.DailyBoxOffice) { + rankLabel.text = data.rank + rankIntensityLabel.attributedText = setUpRankIntensity(data.rankIntensity, isNew: data.rankOldAndNew == "NEW") + movieNameLabel.text = data.movieName + audienceLabel.text = "오늘 \(data.audienceCount.formatToDecimalNumber()) / 총 \(data.audienceAccumulate.formatToDecimalNumber())" + } + + private func setUpRankIntensity(_ rankIntensity: String, isNew: Bool) -> NSMutableAttributedString { + var fixedIntensity = "" + let intRankIntensity = Int(rankIntensity) ?? 0 + + guard !isNew else { + rankIntensityLabel.textColor = .systemRed + return NSMutableAttributedString(string: "신작") + } + + if intRankIntensity == 0 { + fixedIntensity = "-" + } else if intRankIntensity > 0 { + fixedIntensity = "▲\(rankIntensity)" + } else { + fixedIntensity = "▼\(intRankIntensity * -1)" + } + + let attributedString = NSMutableAttributedString(string: fixedIntensity) + + attributedString.addAttribute(.foregroundColor, value: UIColor.systemRed, range: (fixedIntensity as NSString).range(of: "-")) + attributedString.addAttribute(.foregroundColor, value: UIColor.systemRed, range: (fixedIntensity as NSString).range(of: "▲")) + attributedString.addAttribute(.foregroundColor, value: UIColor.systemBlue, range: (fixedIntensity as NSString).range(of: "▼")) + + return attributedString + } + + override func prepareForReuse() { + rankIntensityLabel.textColor = .black + } +} diff --git a/BoxOffice/View/BoxOfficeRankingCell.swift b/BoxOffice/View/BoxOfficeRankingListCell.swift similarity index 95% rename from BoxOffice/View/BoxOfficeRankingCell.swift rename to BoxOffice/View/BoxOfficeRankingListCell.swift index f0db9157..e76ff7f7 100644 --- a/BoxOffice/View/BoxOfficeRankingCell.swift +++ b/BoxOffice/View/BoxOfficeRankingListCell.swift @@ -7,8 +7,8 @@ import UIKit -final class BoxOfficeRankingCell: UICollectionViewListCell { - static let cellIdentifier = "BoxOfficeCell" +final class BoxOfficeRankingListCell: UICollectionViewListCell { + static let cellIdentifier = "BoxOfficeListCell" private let rankLabel: UILabel = { let label = UILabel() @@ -119,4 +119,8 @@ final class BoxOfficeRankingCell: UICollectionViewListCell { return attributedString } + + override func prepareForReuse() { + rankIntensityLabel.textColor = .black + } } From b017a9f7d4f82117e80473c30870ee9ed2de6c00 Mon Sep 17 00:00:00 2001 From: iOS-Yetti Date: Wed, 16 Aug 2023 15:18:41 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=B3=80=EA=B2=BD=EC=8B=9C=20CollectionView?= =?UTF-8?q?=EC=9D=98=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=EB=B0=94=EA=BE=B8=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/BoxOfficeViewController.swift | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/BoxOffice/Controller/BoxOfficeViewController.swift b/BoxOffice/Controller/BoxOfficeViewController.swift index 6dee4ff3..16e706e1 100644 --- a/BoxOffice/Controller/BoxOfficeViewController.swift +++ b/BoxOffice/Controller/BoxOfficeViewController.swift @@ -12,8 +12,7 @@ final class BoxOfficeViewController: UIViewController, URLSessionDelegate { private var refreshControl = UIRefreshControl() private var dataSource: UICollectionViewDiffableDataSource? private var date: Date = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date() - private var isListMode = true - + private let collectionView: UICollectionView = { let configuration = UICollectionLayoutListConfiguration(appearance: .plain) let layout = UICollectionViewCompositionalLayout.list(using: configuration) @@ -44,6 +43,14 @@ final class BoxOfficeViewController: UIViewController, URLSessionDelegate { } } } + + private var isListMode = true { + didSet { + setUpCollectionViewLayout() + setUpDataSource() + passFetchedData() + } + } override func viewDidLoad() { super.viewDidLoad() @@ -63,7 +70,6 @@ extension BoxOfficeViewController { let modeChangeButton = UIBarButtonItem(title: "화면 모드 변경", style: .plain, target: self, action: #selector(hitChangeModeButton)) let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) - view.backgroundColor = .systemBackground view.addSubview(collectionView) view.addSubview(indicatorView) @@ -84,6 +90,25 @@ extension BoxOfficeViewController { ]) } + private func setUpCollectionViewLayout() { + if isListMode { + let configuration = UICollectionLayoutListConfiguration(appearance: .plain) + let layout = UICollectionViewCompositionalLayout.list(using: configuration) + + collectionView.collectionViewLayout = layout + } else { + let layout = UICollectionViewFlowLayout() + let width = (view.frame.width - 45) / 2.0 + + layout.sectionInset = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) + layout.minimumLineSpacing = 10 + layout.minimumInteritemSpacing = 15 + layout.itemSize = CGSize(width: width, height: width) + + collectionView.collectionViewLayout = layout + } + } + @objc func showCalendar(_ sender: UIButton) { let viewController = CalendarViewController(date) viewController.delegate = self @@ -124,15 +149,28 @@ extension BoxOfficeViewController { } private func setUpDataSource() { - dataSource = UICollectionViewDiffableDataSource(collectionView: self.collectionView) { (collectionView, indexPath, data) -> UICollectionViewCell? in - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeRankingListCell.cellIdentifier, for: indexPath) as? BoxOfficeRankingListCell else { - return UICollectionViewCell() + if isListMode { + dataSource = UICollectionViewDiffableDataSource(collectionView: self.collectionView) { (collectionView, indexPath, data) -> UICollectionViewCell? in + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeRankingListCell.cellIdentifier, for: indexPath) as? BoxOfficeRankingListCell else { + return UICollectionViewCell() + } + + cell.setUpLabelText(data) + + return cell } + } else { + dataSource = UICollectionViewDiffableDataSource(collectionView: self.collectionView) { (collectionView, indexPath, data) -> UICollectionViewCell? in + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeRankingIconCell.cellIdentifier, for: indexPath) as? BoxOfficeRankingIconCell else { + return UICollectionViewCell() + } - cell.setUpLabelText(data) + cell.setUpLabelText(data) - return cell + return cell + } } + } private func setUpDataSnapshot(_ data: [BoxOfficeEntity.BoxOfficeResult.DailyBoxOffice]) { From 4d95f8cf00c0a5558e7a3ff80cf98a9726255df2 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Wed, 16 Aug 2023 16:05:12 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=20feat:=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=8B=A4=EC=9D=B4=EB=82=98=EB=AF=B9=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=81=EC=9A=A9,=20auto=20layout=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/BoxOfficeViewController.swift | 6 +-- BoxOffice/View/BoxOfficeRankingIconCell.swift | 7 +++ BoxOffice/View/BoxOfficeRankingListCell.swift | 48 ++++++++++++------- BoxOffice/View/MovieDetailStackView.swift | 4 ++ 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/BoxOffice/Controller/BoxOfficeViewController.swift b/BoxOffice/Controller/BoxOfficeViewController.swift index 16e706e1..c5cc3d4a 100644 --- a/BoxOffice/Controller/BoxOfficeViewController.swift +++ b/BoxOffice/Controller/BoxOfficeViewController.swift @@ -152,7 +152,7 @@ extension BoxOfficeViewController { if isListMode { dataSource = UICollectionViewDiffableDataSource(collectionView: self.collectionView) { (collectionView, indexPath, data) -> UICollectionViewCell? in guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeRankingListCell.cellIdentifier, for: indexPath) as? BoxOfficeRankingListCell else { - return UICollectionViewCell() + return BoxOfficeRankingListCell() } cell.setUpLabelText(data) @@ -162,7 +162,7 @@ extension BoxOfficeViewController { } else { dataSource = UICollectionViewDiffableDataSource(collectionView: self.collectionView) { (collectionView, indexPath, data) -> UICollectionViewCell? in guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeRankingIconCell.cellIdentifier, for: indexPath) as? BoxOfficeRankingIconCell else { - return UICollectionViewCell() + return BoxOfficeRankingIconCell() } cell.setUpLabelText(data) @@ -170,7 +170,6 @@ extension BoxOfficeViewController { return cell } } - } private func setUpDataSnapshot(_ data: [BoxOfficeEntity.BoxOfficeResult.DailyBoxOffice]) { @@ -244,6 +243,7 @@ extension BoxOfficeViewController: BoxOfficeDelegate { func setUpDate(_ date: Date) { self.date = date self.title = getDateString(format: Namespace.dateWithHyphen) + passFetchedData() } } diff --git a/BoxOffice/View/BoxOfficeRankingIconCell.swift b/BoxOffice/View/BoxOfficeRankingIconCell.swift index e672307d..a03513cc 100644 --- a/BoxOffice/View/BoxOfficeRankingIconCell.swift +++ b/BoxOffice/View/BoxOfficeRankingIconCell.swift @@ -15,6 +15,7 @@ class BoxOfficeRankingIconCell: UICollectionViewCell { label.textAlignment = .center label.font = .preferredFont(forTextStyle: .largeTitle) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true return label }() @@ -24,6 +25,7 @@ class BoxOfficeRankingIconCell: UICollectionViewCell { label.textAlignment = .center label.font = .preferredFont(forTextStyle: .callout) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true return label }() @@ -32,13 +34,18 @@ class BoxOfficeRankingIconCell: UICollectionViewCell { let label = UILabel() label.font = .preferredFont(forTextStyle: .title3) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 return label }() private let audienceLabel: UILabel = { let label = UILabel() + label.font = .preferredFont(forTextStyle: .callout) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true + label.adjustsFontSizeToFitWidth = true return label }() diff --git a/BoxOffice/View/BoxOfficeRankingListCell.swift b/BoxOffice/View/BoxOfficeRankingListCell.swift index e76ff7f7..cd77a3c0 100644 --- a/BoxOffice/View/BoxOfficeRankingListCell.swift +++ b/BoxOffice/View/BoxOfficeRankingListCell.swift @@ -15,6 +15,7 @@ final class BoxOfficeRankingListCell: UICollectionViewListCell { label.textAlignment = .center label.font = .preferredFont(forTextStyle: .largeTitle) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true return label }() @@ -24,6 +25,7 @@ final class BoxOfficeRankingListCell: UICollectionViewListCell { label.textAlignment = .center label.font = .preferredFont(forTextStyle: .callout) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true return label }() @@ -32,18 +34,31 @@ final class BoxOfficeRankingListCell: UICollectionViewListCell { let label = UILabel() label.font = .preferredFont(forTextStyle: .title3) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 return label }() private let audienceLabel: UILabel = { let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true + label.adjustsFontSizeToFitWidth = true return label }() - private let stackView: UIStackView = { + private let rankingStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.translatesAutoresizingMaskIntoConstraints = false + + return stackView + }() + + private let informationStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical stackView.translatesAutoresizingMaskIntoConstraints = false @@ -63,26 +78,25 @@ final class BoxOfficeRankingListCell: UICollectionViewListCell { private func setUpUI() { self.accessories = [.outlineDisclosure(options: .init(tintColor: .systemGray))] - stackView.addArrangedSubview(movieNameLabel) - stackView.addArrangedSubview(audienceLabel) - contentView.addSubview(rankLabel) - contentView.addSubview(rankIntensityLabel) - contentView.addSubview(stackView) + rankingStackView.addArrangedSubview(rankLabel) + rankingStackView.addArrangedSubview(rankIntensityLabel) + + informationStackView.addArrangedSubview(movieNameLabel) + informationStackView.addArrangedSubview(audienceLabel) + + contentView.addSubview(rankingStackView) + contentView.addSubview(informationStackView) NSLayoutConstraint.activate([ - rankLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), - rankLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - rankLabel.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.3), - - rankIntensityLabel.widthAnchor.constraint(equalTo: rankLabel.widthAnchor), - rankIntensityLabel.topAnchor.constraint(equalTo: rankLabel.bottomAnchor), - rankIntensityLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - rankIntensityLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), + rankingStackView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.3), + rankingStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + rankingStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - stackView.leadingAnchor.constraint(equalTo: rankLabel.trailingAnchor), - stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), - stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + informationStackView.leadingAnchor.constraint(equalTo: rankLabel.trailingAnchor), + informationStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + informationStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + self.heightAnchor.constraint(equalTo: informationStackView.heightAnchor, constant: 30), self.separatorLayoutGuide.leadingAnchor.constraint(equalTo: rankLabel.leadingAnchor) ]) } diff --git a/BoxOffice/View/MovieDetailStackView.swift b/BoxOffice/View/MovieDetailStackView.swift index 03698e81..a5a39899 100644 --- a/BoxOffice/View/MovieDetailStackView.swift +++ b/BoxOffice/View/MovieDetailStackView.swift @@ -12,14 +12,18 @@ final class MovieDetailStackView: UIStackView { let label = UILabel() label.font = .preferredFont(forTextStyle: .headline) label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 return label }() let valueLabel: UILabel = { let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 + label.adjustsFontForContentSizeCategory = true return label }() From f9c52eab9ff11b6fe17c3fe2bbd5901011a9205b Mon Sep 17 00:00:00 2001 From: iOS-Yetti Date: Thu, 17 Aug 2023 11:36:52 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20APIKey=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BoxOffice/Model/Error.swift | 3 +++ BoxOffice/Model/NetworkingManager.swift | 12 ++++++++---- BoxOffice/Resource/NetworkConfiguration.swift | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/BoxOffice/Model/Error.swift b/BoxOffice/Model/Error.swift index 968912c1..f4065f8e 100644 --- a/BoxOffice/Model/Error.swift +++ b/BoxOffice/Model/Error.swift @@ -10,6 +10,7 @@ enum NetworkingError: Error { case notHttpUrlResponse case invalidResponse(statusCode: Int) case invalidURL + case invalidAPIKey var description: String { switch self { @@ -21,6 +22,8 @@ enum NetworkingError: Error { return "서버 응답 오류입니다. Status Code: \(statusCode)" case .invalidURL: return "유효하지 않은 URL입니다." + case .invalidAPIKey: + return "유효하지 않은 API Key입니다." } } } diff --git a/BoxOffice/Model/NetworkingManager.swift b/BoxOffice/Model/NetworkingManager.swift index 7f418c7a..c6c1c748 100644 --- a/BoxOffice/Model/NetworkingManager.swift +++ b/BoxOffice/Model/NetworkingManager.swift @@ -15,7 +15,11 @@ struct NetworkingManager { } func load(_ networkType: NetworkConfiguration, completion: @escaping (Result) -> Void) { - var urlComponents = URLComponents(string: networkType.url) + guard let urlString = networkType.url, let header = networkType.header else { + completion(.failure(NetworkingError.invalidAPIKey)) + return + } + var urlComponents = URLComponents(string: urlString) networkType.query.forEach { urlComponents?.queryItems = [URLQueryItem(name: $0.name, value: $0.value)] @@ -23,12 +27,12 @@ struct NetworkingManager { guard let url = urlComponents?.url else { completion(.failure(NetworkingError.invalidURL)) - return + return } var request = URLRequest(url: url) - - networkType.header.forEach { + + header.forEach { request.setValue($0.value, forHTTPHeaderField: $0.forHTTPHeaderField) } diff --git a/BoxOffice/Resource/NetworkConfiguration.swift b/BoxOffice/Resource/NetworkConfiguration.swift index d97ba118..e21664a2 100644 --- a/BoxOffice/Resource/NetworkConfiguration.swift +++ b/BoxOffice/Resource/NetworkConfiguration.swift @@ -12,11 +12,19 @@ enum NetworkConfiguration: Hashable { case movieDetail(_ movieCode: String) case daumImage(_ movieName: String) - var url: String { + var url: String? { switch self { case .boxOffice(let targetDate): + if NetworkConfiguration.apiKey == "" { + return nil + } + return String(format: "http://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=%@&targetDt=%@", NetworkConfiguration.apiKey, targetDate) case .movieDetail(let movieCode): + if NetworkConfiguration.apiKey == "" { + return nil + } + return String(format: "http://www.kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieInfo.json?key=%@&movieCd=%@", NetworkConfiguration.apiKey, movieCode) case .daumImage: return "https://dapi.kakao.com/v2/search/image" @@ -32,9 +40,13 @@ enum NetworkConfiguration: Hashable { } } - var header: [(value: String, forHTTPHeaderField: String)] { + var header: [(value: String, forHTTPHeaderField: String)]? { switch self { case .daumImage: + if NetworkConfiguration.daumApiKey == "" { + return nil + } + return [(value: "KakaoAK \(NetworkConfiguration.daumApiKey)", forHTTPHeaderField: "Authorization")] default: return [] From 3e22b97373fb41b0269ef46cf745df32893f6086 Mon Sep 17 00:00:00 2001 From: Min Hyun Date: Thu, 17 Aug 2023 11:42:16 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=EB=82=A0=EC=A7=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=8B=9C=20=EC=BB=AC=EB=A0=89=EC=85=98=EB=B7=B0=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=ED=8B=80=EC=96=B4?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BoxOffice/Controller/BoxOfficeViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BoxOffice/Controller/BoxOfficeViewController.swift b/BoxOffice/Controller/BoxOfficeViewController.swift index c5cc3d4a..83c76362 100644 --- a/BoxOffice/Controller/BoxOfficeViewController.swift +++ b/BoxOffice/Controller/BoxOfficeViewController.swift @@ -243,7 +243,9 @@ extension BoxOfficeViewController: BoxOfficeDelegate { func setUpDate(_ date: Date) { self.date = date self.title = getDateString(format: Namespace.dateWithHyphen) - + + setUpCollectionViewLayout() + setUpDataSource() passFetchedData() } } From bfa29d993bd6d84585bf80aff09f9255aa565a18 Mon Sep 17 00:00:00 2001 From: Max Hyun <73330542+maxhyunm@users.noreply.github.com> Date: Fri, 18 Aug 2023 10:46:57 +0900 Subject: [PATCH 7/7] docs: Update README.md --- README.md | 384 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 325 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index eddf3383..aecb9682 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,30 @@ # 박스오피스🎬 ## 소개 -> 영화진흥위원회의 API를 통해 박스오피스, 영화 상세정보를 불러오는 앱입니다. +> 영화진흥위원회와 Daum 검색 API를 통해 날짜별 박스오피스, 영화 상세정보를 불러오는 앱입니다. -**프로젝트 기간 : 23/07/24~** +**프로젝트 기간 : 23/07/24~23/08/18**
## 목차 -1. [팀원소개](#1.) -2. [타임라인](#2.) -3. [시각화구조](#3.) -4. [핵심경험](#4.) -5. [트러블슈팅](#5.) -6. [참고자료](#6.) -7. [팀 회고](#7.) +1. [팀원 소개](#1.) +2. [타임 라인](#2.) +3. [시각화 구조](#3.) +4. [실행 화면](#4. ) +5. [핵심 경험](#5.) +6. [트러블 슈팅](#6.) +7. [참고 자료](#7.) +8. [팀 회고](#8.)

-## 팀원소개 +## 팀원 소개 ||| |:-:|:-:| |[**Yetti**](https://github.com/iOS-Yetti)|[**maxhyunm**](https://github.com/maxhyunm)|
-## 타임라인 +## 타임 라인 |날짜|내용| |:--:|--| |2023.07.24.| json 파일 파싱을 위한 BoxOfficeEntity 타입 생성, 디코딩 유닛테스트 진행 | @@ -33,7 +34,16 @@ |2023.07.31.| 프로젝트 진행을 위한 개인 공부시간 | |2023.08.01.| 프로젝트 진행을 위한 개인 공부시간 | |2023.08.02.| CollectionView세팅, BoxOfficeRankingCell 생성 및 셀 구성 세팅, DiffableDataSource 세팅 및 연결, 랭킹 증감분 AttributedString 처리, collectionView에 refreshControl 추가 | -|2023.08.04.| 리뷰에 따른 리팩토링 진행 | +|2023.08.07.| 리뷰에 따른 리팩토링 진행 | +|2023.08.08.|DaumImageEntity 파일 추가 및 DAUM_API_KEY 추가, MovieDetailViewController 파일 추가 및 view 전환 메서드 추가, MoviewDetailViewController에 MoviewDetail 네트워크 연결 | +|2023.08.09.|프로젝트 진행을 위한 개인 공부시간| +|2023.08.10.|스택뷰의 text설정메서드 기능 분리 및 Namespace 생성, MovieDetailView 로딩화면 수정 | +|2023.08.11.|README 작성| +|2023.08.14.|url 호출 실패시 completion으로 에러처리 추가, MovieDetailViewController에서 isDataLoading, isImageLoading 호출 위치 변경| +|2023.08.15.|프로젝트 진행을 위한 개인 공부시간| +|2023.08.16.|화면 모드 변경 버튼 추가 BoxOfficeRankingIconCell 타입 생성, 화면 모드 변경시 CollectionView의 레이아웃 설정을 바꾸는 기능 구현, 텍스트에 다이나믹 타입 적용 및 auto layout 수정| +|2023.08.17.|APIKey 검증과정 및 에러처리 추가, 날짜 변경시 컬렉션뷰 레이아웃 틀어지는 오류 수정| +|2023.08.18.|README 작성|
## 시각화 구조 @@ -44,22 +54,27 @@ │   │   └── String+.swift │   ├── Model │   │   ├── BoxOfficeEntity.swift + │   │   ├── MovieDetailEntity.swift + │   │   ├── DaumImageEntity.swift │   │   ├── DecodingManager.swift │   │   ├── Error.swift - │   │   ├── MovieDetailEntity.swift │   │   ├── NetworkingManager.swift │   │   └── URLSessionProtocol.swift │   ├──View - │   │ └── BoxOfficeRankingCell.swift + │   │ ├── BoxOfficeRankingListCell.swift + │   │ ├── BoxOfficeRankingIconCell.swift + │   │ └── MovieDetailStackView.swift │   ├── Controller - │   │   └── BoxOfficeViewController.swift + │   │   ├── BoxOfficeViewController.swift + │   │   ├── CalendarViewController.swift + │   │   └── MovieDetailViewController.swift │   ├── Resource │   │   ├── AppDelegate.swift │   │   ├── SceneDelegate.swift - │   │   ├── NetworkNamespace.swift + │   │   ├── NetworkConfiguration.swift │   │   ├── Assets.xcassets │   │   └── box_office_sample.json - │   └── Info.plist + │   └──Info.plist ├── BoxOffice.xcodeproj ├── BoxOfficeTests │   ├── BoxOffice.xctestplan @@ -69,29 +84,256 @@ └── README.md ### UML -
+#### 박스오피스 화면 +
+#### 영화 상세정보 화면 + +

-## 핵심경험 +## 실행 화면 + +| 영화 상세정보 | 날짜 변경 | 화면 모드 변경 | +|:--------:|:--------:|:--------:| +|||| + + + + +
+## 핵심 경험 +#### 🌟 xcconfig, info.plist를 활용한 api key 설정 +환경 파일을 활용해 원격 저장소에 공유되지 않아야 하는 key 정보를 관리하였습니다. + #### 🌟 CodingKeys와 Nested Type Enum을 활용한 중첩 json 파싱 `Nested Type`을 활용하여 여러 단계로 중첩된 형태의 json을 파싱할 수 있도록 하였고, `CodingKeys`를 활용해 이해하기 어려운 파라미터명을 변경하였습니다. +
+상세코드 +
+ +```swift +extension BoxOfficeEntity { + struct BoxOfficeResult: Decodable { + let boxOfficeType, showRange: String + let dailyBoxOfficeList: [DailyBoxOffice] + + enum CodingKeys: String, CodingKey { + case boxOfficeType = "boxofficeType" + case showRange, dailyBoxOfficeList + } + } +} +``` +
+
+ #### 🌟 Generic을 활용한 범용 메서드 구현 다양한 타입의 Entity를 반환해야 하는 `DecodingManager`의 메서드를 `Generic`으로 구현하였습니다. -#### 🌟 xcconfig, info.plist를 활용한 api key 설정 -환경 파일을 활용해 원격 저장소에 공유되지 않아야 하는 key 정보를 관리하였습니다. +
+상세코드 +
+ +```swift +func decode(_ data: Data?) throws -> T { + guard let data = data, + let decodedData = try? decoder.decode(T.self, from: data) else { + throw DecodingError.decodingFailure + } + return decodedData +} +``` + +
+
+ +#### 🌟 Modern Collection View 구현 +`Modern Collection View`를 통해 박스오피스 랭킹 리스트를 구현하기 위하여 `Diffable Data Source`와 `Collection View List Cell`를 활용하였습니다. +
+상세코드 +
+ +```swift +private let collectionView: UICollectionView = { + let configuration = UICollectionLayoutListConfiguration(appearance: .plain) + let layout = UICollectionViewCompositionalLayout.list(using: configuration) +... +} + +private var dataSource: UICollectionViewDiffableDataSource? +... + +``` + +
+
+ +#### 🌟 UICalendarView 활용 +`UICalendarView`를 활용해 `Calendar`를 구현하고 과거 날짜의 데이터를 불러올 수 있도록 활용하였습니다. +
+상세코드 +
+ +```swift +private let calendarView: UICalendarView = { + var calendarView = UICalendarView() + calendarView.translatesAutoresizingMaskIntoConstraints = false + calendarView.backgroundColor = .systemBackground + + let endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date() + let calendarViewDateRange = DateInterval(start: Date(timeIntervalSince1970: 0), end: endDate) + calendarView.availableDateRange = calendarViewDateRange + + return calendarView + }() +``` + +
+
+ #### 🌟 UIActivityIndicatorView를 활용한 로딩 구현 데이터 fetch 상태에 따라 `UIActivityIndicatorView`의 상태값을 변경하여 로딩 마크가 활성화/비활성화 되도록 구현하였습니다. +
+상세코드 +
+ +```swift +private let indicatorView: UIActivityIndicatorView = { + let indicatorView = UIActivityIndicatorView() + indicatorView.style = .large + indicatorView.translatesAutoresizingMaskIntoConstraints = false + + return indicatorView +}() + +private var isLoading: Bool = true { + willSet(newValue) { + if newValue == true { + indicatorView.isHidden = false + indicatorView.startAnimating() + } else { + indicatorView.isHidden = true + indicatorView.stopAnimating() + } + } +} +``` + +
+
+ #### 🌟 UIRefreshControl를 활용한 새로고침 구현 `Collection View`에 `UIRefreshColtrol` 객체를 추가하여, 아래로 당겼을 때 새로고침을 진행할 수 있도록 하였습니다. -#### 🌟 Modern Collection View 구현 -`Modern Collection View`를 통해 박스오피스 랭킹 리스트를 구현하기 위하여 `Diffable Data Source`와 `Collection View List Cell`를 활용하였습니다. +
+상세코드 +
+ +```swift +collectionView.refreshControl = refreshControl + refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) +``` + +
+
+ +#### 🌟 UIToolBar 활용 +`UIToolBar`와 `flexibleSpace`를 활용하여 화면 하단 버튼을 구현하였습니다. + +
+상세코드 +
+ +```swift +let modeChangeButton = UIBarButtonItem(title: "화면 모드 변경", style: .plain, target: self, action: #selector(hitChangeModeButton)) +let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) +... +self.toolbarItems = [flexibleSpace, modeChangeButton, flexibleSpace] +``` + +
+
+ +#### 🌟 UIAlertController 활용 +화면 모드 변경시 `UIAlertController`의 `actionSheet`스타일로 아이콘 모드와 리스트 모드 화면을 선택적으로 적용할 수 있도록 구현하였습니다. +
+상세코드 +
+ +```swift +@objc func hitChangeModeButton() { + let mode: String = isListMode == true ? "아이콘" : "리스트" + let alert = UIAlertController(title: "화면 모드 변경", message: nil, preferredStyle: .actionSheet) + let modeChangeAction = UIAlertAction(title: mode, style: .default) { _ in + self.isListMode.toggle() + } + let cancelAction = UIAlertAction(title: "취소", style: .cancel) + + alert.addAction(modeChangeAction) + alert.addAction(cancelAction) + present(alert, animated: true) +} +``` + +
+
+ + #### 🌟 Attributed String 활용 하나의 레이블 안에서 여러 가지 색상을 표시하기 위하여 `Attributed String`을 활용하였습니다. +
+상세코드 +
+ +```swift +attributedString.addAttribute(.foregroundColor, value: UIColor.systemRed, range: (fixedIntensity as NSString).range(of: "▲")) +``` + +
+
-
-## 트러블슈팅 -### 1️⃣ dataTask 메서드로 받아온 데이터 처리 +#### 🌟 DynamicType 적용 +`preferredFont`를 활용하여 폰트에 `DynamicType`을 적용하였고, `adjustsFontSizeToFitWidth` 설정을 통해 가로 너비에 맞춰 텍스트 크기를 자동 조절할 수 있도록 구현하였습니다. +
+상세코드 +
+ +```swift +private let audienceLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true + label.adjustsFontSizeToFitWidth = true + + return label +}() +``` + +
+
+ +
+## 트러블 슈팅 +### 1️⃣ 네트워크 연결시 URL 전달 방식 +**🚨 문제점**
+기존에는 `dataTask` 메서드에 `URL` 타입을 전달하여 네트워크 연결을 진행하도록 구현했었습니다. 하지만 이번에 `Daum API`가 추가되면서 `Authorization` 헤더를 추가해야 했기 때문에 해당 정보를 어떻게 전달할지 고민하였습니다. + +**💡 해결 방법**
+`URLRequest`를 활용하면 `setValue`를 통해 헤더 정보를 추가할 수 있음을 확인하였습니다. 이에 따라 기존 테스트더블 및 테스트코드 모두 `URLRequest`에 맞게 수정을 진행하였습니다. + +```swift +var request = URLRequest(url: url) +request.setValue("KakaoAK \(NetworkNamespace.daumApiKey)", forHTTPHeaderField: "Authorization") +``` + +또한 `Daum API`에 전달할 쿼리 내용에는 띄어쓰기가 추가되어 있어, `URL`에 붙여서 보내기보다는 `URLComponents`를 사용해 필요한 쿼리 아이템을 추가하는 게 좋겠다는 생각이 들었습니다. +해당 내용은 아래와 같이 구현하였습니다. +```swift +var urlComponents = URLComponents(string: NetworkNamespace.daumImage.url) +urlComponents?.queryItems = [URLQueryItem(name: "query", value: "\(movieName) 영화 포스터")] +``` + +### 2️⃣ dataTask 메서드로 받아온 데이터 처리 **🚨 문제점**
`NetworkingManager`의 `load()` 메서드를 호출한 위치에서 `dataTask`를 통해 받아 온 데이터를 활용할 수 있는 방법에 대해 고민이 있었습니다. 처음에는 두 타입을 델리게이트로 연결하여 전달하는 등의 방법을 생각했습니다. 하지만 데이터 처리를 위해 네트워킹을 사용하는 모든 타입을 델리게이트로 연결하는 것은 권장되는 방식도 아니고, 효율적이지 못한 것 같았습니다. @@ -112,7 +354,7 @@ func load(_ urlString: String, completion: @escaping (Result API를 받아와야 하는 도메인이 `https`가 아닌 `http`를 활용하고 있어 네트워크 연결시에 오류가 발생하였습니다.

@@ -121,7 +363,7 @@ API를 받아와야 하는 도메인이 `https`가 아닌 `http`를 활용하고 해당 도메인 및 하위 도메인 정보를 ATS에 `Exception Domains`로 추가하여 정상적으로 네트워킹이 가능하도록 구현하였습니다.

-### 3️⃣ 로딩과 새로고침 종료 위치 설정 +### 4️⃣ 로딩과 새로고침 종료 위치 설정 **🚨 문제점**
네트워킹을 통해 받아 온 데이터를 처리하는 과정에서 성공한 경우에만 로딩을 끝내고 빠져나갈 수 있도록 처리하였더니, 에러가 났을 때는 아래와 같이 계속 로딩이 돌아가고 있는 것처럼 보이는 것을 확인했습니다.

@@ -151,33 +393,17 @@ DispatchQueue.main.async { } ``` -### 4️⃣ Cell Accessory 설정 +### 5️⃣ 셀 재활용으로 인한 문제 해결 **🚨 문제점**
-아래와 같이 코드를 작성하여 각 셀 우측에 악세서리를 추가하였습니다. 하지만 이렇게 하니 `>` 아이콘이 파란색으로 표시되는 것을 확인하였습니다. -```swift -cell.accessories = [.outlineDisclosure()] -``` -
- +Collection View에서 셀을 재사용하면서, 검은 색으로 들어가야 하는 순위 텍스트가 빨간색으로 잘못 들어가는 문제가 있었습니다. +
**💡 해결 방법**
-`tintColor` 설정을 추가하여 회색으로 표시될 수 있도록 변경하였습니다. +`PrepareForReuse()`를 통해 아래와 같이 폰트 색상을 초기화해 주었습니다. ```swift -cell.accessories = [.outlineDisclosure(options: .init(tintColor: .systemGray))] -``` -
- -### 5️⃣ Cell Separator 설정 -**🚨 문제점**
-`UICollectionLayoutListConfiguration`의 기본 설정으로 `Collection View`를 구성하니 각 셀 사이의 구분자인 `separator`가 화면 끝까지 이어지지 않는 문제가 있었습니다.
-
- -**💡 해결 방법**
-레이아웃 설정에서 아래의 내용을 추가하여 해결하였습니다. -```swift -self.separatorLayoutGuide.leadingAnchor.constraint(equalTo: rankLabel.leadingAnchor) +override func prepareForReuse() { + rankIntensityLabel.textColor = .black +} ``` -
- ### 6️⃣ Test Double 생성 **🚨 문제점**
인터넷 연결이 없는 상태에서 네트워크 통신을 테스트하기 위해 `Test Double`을 생성하였습니다. 이 과정에서 테스트용 `Stub Session`과 실제 `Session` 사이에 호환이 가능하도록 하기 위해 `URLSessionProtocol`을 구현하였는데, `URLSession`에서 이를 상속하려 하니 아래와 같은 경고가 발생하였습니다.
@@ -190,17 +416,57 @@ typealias CompletionHandler = @Sendable (Data?, URLResponse?, Error?) -> Void ``` -
-## 참고자료 -- [URLSession 공식문서🍎](https://developer.apple.com/documentation/foundation/urlsession) -- [Fetching Website Data into Memory 공식문서🍎](https://developer.apple.com/documentation/foundation/url_loading_system/fetching_website_data_into_memory) -- [UICollectionView 공식문서🍎](https://developer.apple.com/documentation/uikit/uicollectionview) -- [UICollectionViewListCell 공식문서🍎](https://developer.apple.com/documentation/uikit/uicollectionviewlistcell) -- [UICollectionViewDiffableDataSource 공식문서🍎](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource) +### 7️⃣ 다양한 네트워크에 대한 Test 환경 구성 +**🚨 문제점**
+세 가지 다른 API를 호출하는 프로그램이다 보니, 각 API 내용에 따라 매번 새로운 테스트 환경을 만들어주어야 해서 번거로웠습니다. + +**💡 해결 방법**
+아래와 같이 테스트 타입을 파라미터로 전달받아 각 타입에 맞는 테스트환경을 구성할 수 있도록 구현하였습니다. +```swift +func setUpSUT(isSuccess: Bool, apiType: NetworkNamespace) { + ... + switch apiType { + case .boxOffice: + urlString = String(format: NetworkNamespace.boxOffice.url, NetworkNamespace.apiKey, "20230801") + asset = "box_office_sample" + case .movieDetail: + urlString = String(format: NetworkNamespace.movieDetail.url, NetworkNamespace.apiKey, "20230801") + asset = "movie_detail_sample" + case .daumImage: + urlString = String(format: NetworkNamespace.daumImage.url) + asset = "daum_image_sample" + } + ... + + if isSuccess { + ... + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) + ... + } else { + let response = HTTPURLResponse(url: url, statusCode: 400, httpVersion: nil, headerFields: nil) + ... + } +} +``` + +
+## 참고 자료 +- [URLSession 공식문서 🍎](https://developer.apple.com/documentation/foundation/urlsession) +- [Fetching Website Data into Memory 공식문서 🍎](https://developer.apple.com/documentation/foundation/url_loading_system/fetching_website_data_into_memory) +- [UICollectionView 공식문서 🍎](https://developer.apple.com/documentation/uikit/uicollectionview) +- [UICollectionViewListCell 공식문서 🍎](https://developer.apple.com/documentation/uikit/uicollectionviewlistcell) +- [UICollectionViewDiffableDataSource 공식문서 🍎](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource) +- [URLRequest 공식문서 🍎](https://developer.apple.com/documentation/foundation/urlrequest) +- [URLComponents 공식문서 🍎](https://developer.apple.com/documentation/foundation/urlcomponents) +- [AttributedString 공식문서 🍎](https://developer.apple.com/documentation/foundation/attributedstring) +- [UIRefreshControl 공식문서 🍎](https://developer.apple.com/documentation/uikit/uirefreshcontrol) +- [UIActivityIndicatorView 공식문서 🍎](https://developer.apple.com/documentation/uikit/uiactivityindicatorview) +- [UICalendarView 공식문서 🍎](https://developer.apple.com/documentation/uikit/uicalendarview) +- [UIAlertController 공식문서 🍎](https://developer.apple.com/documentation/uikit/uialertcontroller) - [야곰 닷넷 - Unit Test](https://yagom.net/courses/unit-test-%ec%9e%91%ec%84%b1%ed%95%98%ea%b8%b0/)
-
+
## 팀 회고 [일일 스크럼](https://github.com/iOS-Yetti/ios-box-office/wiki)