diff --git a/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj b/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj index cda0be80..7342259f 100644 --- a/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj +++ b/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 09A146652A1964B500DDC308 /* AddAnotherDayResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A146642A19649A00DDC308 /* AddAnotherDayResponseDTO.swift */; }; 09C8602D2AB14B4800C4F4B1 /* FSCalendar in Frameworks */ = {isa = PBXBuildFile; productRef = 09C8602C2AB14B4800C4F4B1 /* FSCalendar */; }; 09CF56042B09F23800526C8C /* HomeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CF56032B09F23800526C8C /* HomeDataSource.swift */; }; + 09CF56022B09E98A00526C8C /* DetailAchieveHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CF56012B09E98A00526C8C /* DetailAchieveHeaderView.swift */; }; 09DCCD1F2A18ED76003DCF8A /* DailyMissionResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DCCD1E2A18ED76003DCF8A /* DailyMissionResponseDTO.swift */; }; 09DCCD212A18EF43003DCF8A /* HomeSevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DCCD202A18EF43003DCF8A /* HomeSevice.swift */; }; 09DCCD232A18EFB0003DCF8A /* HomeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DCCD222A18EFB0003DCF8A /* HomeAPI.swift */; }; @@ -209,6 +210,7 @@ 09A1465E2A192C4900DDC308 /* WeekMissionResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekMissionResponseDTO.swift; sourceTree = ""; }; 09A146642A19649A00DDC308 /* AddAnotherDayResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAnotherDayResponseDTO.swift; sourceTree = ""; }; 09CF56032B09F23800526C8C /* HomeDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeDataSource.swift; sourceTree = ""; }; + 09CF56012B09E98A00526C8C /* DetailAchieveHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAchieveHeaderView.swift; sourceTree = ""; }; 09DCCD1E2A18ED76003DCF8A /* DailyMissionResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyMissionResponseDTO.swift; sourceTree = ""; }; 09DCCD202A18EF43003DCF8A /* HomeSevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSevice.swift; sourceTree = ""; }; 09DCCD222A18EFB0003DCF8A /* HomeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAPI.swift; sourceTree = ""; }; @@ -590,6 +592,7 @@ 3B027AA1299C355800BEB65C /* AchievementViewController.swift */, 09582B5029C0BC3600EF3207 /* DetailAchievementViewController.swift */, 0930D37229B4FCAE0000C4AE /* StatisticsView.swift */, + 09CF56012B09E98A00526C8C /* DetailAchieveHeaderView.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1332,6 +1335,7 @@ 3B11740D2A4B574B0033DDF3 /* CALayer+.swift in Sources */, 3B14A13D29A6FBD300F92897 /* UIView+.swift in Sources */, 09F6719529CBFCD200708725 /* GradientView.swift in Sources */, + 09CF56022B09E98A00526C8C /* DetailAchieveHeaderView.swift in Sources */, 3B4E12F82A27C12F001D1EC1 /* WithdrawModalView.swift in Sources */, 6CA208252A18FEEA001C4247 /* RecommendService.swift in Sources */, 3B482FA5299EAB8800BCF424 /* TabBarController.swift in Sources */, diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift index c4a392c5..c665491c 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift @@ -7,7 +7,10 @@ import UIKit -class DetailAchievementCollectionViewCell: UICollectionViewCell { +import SnapKit +import Then + +final class DetailAchievementCollectionViewCell: UICollectionViewCell { // MARK: - Properties @@ -16,8 +19,6 @@ class DetailAchievementCollectionViewCell: UICollectionViewCell { // MARK: - UI Components let tagLabel = PaddingLabel(padding: UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12)) - private let horizontalStackView = UIStackView() - private let emptyView = UIView() let titleLabel = UILabel() private let checkImage = UIImageView() @@ -25,6 +26,7 @@ class DetailAchievementCollectionViewCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) + setUI() setLayout() } @@ -37,55 +39,57 @@ class DetailAchievementCollectionViewCell: UICollectionViewCell { // MARK: - Methods extension DetailAchievementCollectionViewCell { + private func setUI() { + contentView.backgroundColor = .clear tagLabel.do { $0.layer.backgroundColor = UIColor.bg?.cgColor $0.font = .Pretendard(.medium, size: 14) $0.textColor = .gray1 - $0.layer.cornerRadius = 10 + $0.layer.cornerRadius = 25/2 } - - horizontalStackView.do { - $0.addArrangedSubviews(titleLabel, emptyView, checkImage) - $0.axis = .horizontal - } - + titleLabel.do { $0.font = .Pretendard(.semiBold, size: 16) $0.textColor = .gray2 - $0.numberOfLines = 0 + $0.numberOfLines = 1 $0.textAlignment = .left } + checkImage.do { $0.image = .icChecked } } + private func setLayout() { - addSubviews(tagLabel, horizontalStackView) - + contentView.addSubviews(tagLabel, titleLabel, checkImage) + tagLabel.snp.makeConstraints { $0.top.equalToSuperview().offset(22) - $0.leading.equalToSuperview().offset(29) + $0.leading.equalToSuperview().inset(28) } - - horizontalStackView.snp.makeConstraints { + + titleLabel.snp.makeConstraints { $0.top.equalTo(tagLabel.snp.bottom).offset(7) $0.leading.equalToSuperview().inset(28) - $0.trailing.equalToSuperview() - $0.bottom.equalToSuperview().inset(24) + $0.trailing.equalToSuperview().inset(50) } checkImage.snp.makeConstraints { - $0.trailing.equalToSuperview() + $0.centerY.equalTo(titleLabel.snp.centerY) $0.size.equalTo(21) + $0.trailing.equalToSuperview().inset(28) + $0.bottom.equalToSuperview().inset(24) } } func configure(model: DailyMissionResponseDTO) { - tagLabel.text = model.title - titleLabel.text = model.situationName + tagLabel.text = model.situationName + titleLabel.text = model.title + titleLabel.lineBreakMode = .byTruncatingTail + switch model.completionStatus { case .CHECKED: checkImage.isHidden = false case .UNCHECKED: checkImage.isHidden = true diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchieveHeaderView.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchieveHeaderView.swift new file mode 100644 index 00000000..a638dd51 --- /dev/null +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchieveHeaderView.swift @@ -0,0 +1,64 @@ +// +// DetailAchieveHeaderView.swift +// iOS-NOTTODO +// +// Created by JEONGEUN KIM on 11/19/23. +// + +import UIKit + +import SnapKit +import Then + +final class DetailAchieveHeaderView: UICollectionReusableView { + + // MARK: - Properties + + static let identifier = "DetailAchieveHeaderView" + + // MARK: - UI Components + + private let dateLabel = UILabel() + + // MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: .zero) + + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +// MARK: - Method + +extension DetailAchieveHeaderView { + + private func setUI() { + + dateLabel.do { + $0.font = .Pretendard(.semiBold, size: 18) + $0.textColor = .gray2 + $0.textAlignment = .center + } + } + + private func setLayout() { + addSubview(dateLabel) + + dateLabel.snp.makeConstraints { + $0.top.equalToSuperview() + $0.centerX.equalToSuperview() + $0.bottom.equalToSuperview().inset(12) + } + } + + func configure(text: String) { + dateLabel.text = text + } +} diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift index 56cb9f3d..697ffb67 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift @@ -14,38 +14,44 @@ final class DetailAchievementViewController: UIViewController { // MARK: - Properties - var missionList: [DailyMissionResponseDTO] = [] - private var mission: String? - private var goal: String? - var selectedDate: Date? + typealias CellRegistration = UICollectionView.CellRegistration + typealias HeaderRegistration = UICollectionView.SupplementaryRegistration + typealias Item = DailyMissionResponseDTO + typealias DataSource = UICollectionViewDiffableDataSource + typealias SnapShot = NSDiffableDataSourceSnapshot + enum Section: Int, Hashable { case main } - typealias Item = AnyHashable - var dataSource: UICollectionViewDiffableDataSource! = nil + + var selectedDate: Date? + + private var dataSource: DataSource? + private lazy var safeArea = self.view.safeAreaLayoutGuide // MARK: - UI Components - private let backGroundView = UIView() - private let dateLabel = UILabel() - private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout()) + private var collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()) // MARK: - Life Cycle override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if let selectedDate = selectedDate { - requestDetailAPI(date: Utils.dateFormatterString(format: "YYYY-MM-dd", date: selectedDate)) + requestDetailAPI(date: Utils.dateFormatterString(format: "YYYY-MM-dd", + date: selectedDate)) } } + override func viewDidLoad() { super.viewDidLoad() - register() + setUI() setLayout() - setupDataSource() - reloadData() + setDataSource() + setSnapShot() } override func touchesBegan(_ touches: Set, with event: UIEvent?) { @@ -53,7 +59,7 @@ final class DetailAchievementViewController: UIViewController { let touch = touches.first! let location = touch.location(in: self.view) - if !backGroundView.frame.contains(location) { + if !collectionView.frame.contains(location) { AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Achieve.closeDailyMissionModal) self.dismiss(animated: true) } @@ -63,66 +69,59 @@ final class DetailAchievementViewController: UIViewController { // MARK: - Methods extension DetailAchievementViewController { - private func register() { - collectionView.register(DetailAchievementCollectionViewCell.self, forCellWithReuseIdentifier: DetailAchievementCollectionViewCell.identifier) - } private func setUI() { view.backgroundColor = .black.withAlphaComponent(0.6) - backGroundView.do { + collectionView.do { + $0.collectionViewLayout = layout() $0.layer.cornerRadius = 15 $0.backgroundColor = .white - $0.isUserInteractionEnabled = false - } - dateLabel.do { - if let selectedDate = selectedDate { - $0.text = Utils.dateFormatterString(format: "YYYY년 MM월 dd일", date: selectedDate) - } - $0.font = .Pretendard(.semiBold, size: 18) - $0.textColor = .gray2 - $0.textAlignment = .center - } - collectionView.do { - $0.backgroundColor = .clear $0.bounces = false $0.autoresizingMask = [.flexibleWidth, .flexibleHeight] } } - + private func setLayout() { - view.addSubview(backGroundView) - backGroundView.addSubviews(dateLabel, collectionView) + view.addSubview(collectionView) - backGroundView.snp.makeConstraints { + collectionView.snp.makeConstraints { $0.center.equalTo(safeArea) $0.directionalHorizontalEdges.equalTo(safeArea).inset(15) $0.height.equalTo(getDeviceWidth()*1.1) } - dateLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(26) - $0.centerX.equalToSuperview() - } - collectionView.snp.makeConstraints { - $0.top.equalTo(dateLabel.snp.bottom) - $0.leading.equalToSuperview() - $0.trailing.equalToSuperview().inset(15) - $0.bottom.equalToSuperview() - } } - private func setupDataSource() { - dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DetailAchievementCollectionViewCell.identifier, for: indexPath) as? DetailAchievementCollectionViewCell else { return UICollectionViewCell() } - cell.configure(model: item as! DailyMissionResponseDTO) - return cell + private func setDataSource() { + + let cellRegistration = CellRegistration {cell, _, item in + cell.configure(model: item) + } + + let headerRegistration = HeaderRegistration(elementKind: UICollectionView.elementKindSectionHeader) { headerView, _, _ in + if let date = self.selectedDate { + headerView.configure(text: Utils.dateFormatterString(format: "YYYY년 MM월 dd일", + date: date)) + } + } + + dataSource = DataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, + for: indexPath, + item: item) }) + + dataSource?.supplementaryViewProvider = { collectionView, _, indexPath in + return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, + for: indexPath) + } } - private func reloadData() { - var snapShot = NSDiffableDataSourceSnapshot() + private func setSnapShot() { + + var snapShot = SnapShot() defer { - dataSource.apply(snapShot, animatingDifferences: false) + dataSource?.apply(snapShot, animatingDifferences: false) } snapShot.appendSections([.main]) @@ -130,30 +129,45 @@ extension DetailAchievementViewController { } private func updateData(item: [DailyMissionResponseDTO]) { - var snapshot = dataSource.snapshot() + + guard var snapshot = dataSource?.snapshot() else { return } + snapshot.appendItems(item, toSection: .main) - dataSource.apply(snapshot) + dataSource?.apply(snapshot) + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Achieve.appearDailyMissionModal(total: item.count)) } private func layout() -> UICollectionViewCompositionalLayout { - var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + + var config = UICollectionLayoutListConfiguration(appearance: .plain) + config.headerMode = .supplementary config.backgroundColor = .clear config.separatorConfiguration.color = .gray5! - let listLayout = UICollectionViewCompositionalLayout.list(using: config) - return listLayout + config.separatorConfiguration.topSeparatorVisibility = .hidden + config.separatorConfiguration.bottomSeparatorInsets = .init(top: 0, + leading: 20, + bottom: 0, + trailing: 20) + config.itemSeparatorHandler = { indexPath, config in + var config = config + guard let itemCount = self.dataSource?.snapshot().itemIdentifiers(inSection: .main).count else { return config } + let isLastItem = indexPath.item == itemCount - 1 + config.bottomSeparatorVisibility = isLastItem ? .hidden : .visible + return config + } + + return UICollectionViewCompositionalLayout.list(using: config) } } extension DetailAchievementViewController { - private func requestDetailAPI(date: String) { HomeAPI.shared.getDailyMission(date: date) { [weak self] response in guard let self else { return } guard let response = response else { return } guard let data = response.data else { return } - let missionList = data - self.updateData(item: missionList) + self.updateData(item: data) } } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift index 3f989243..4b56838f 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift @@ -10,7 +10,7 @@ import UIKit import Then import SnapKit -class StatisticsView: UIView { +final class StatisticsView: UIView { // MARK: - UI Components @@ -21,6 +21,7 @@ class StatisticsView: UIView { override init(frame: CGRect) { super.init(frame: .zero) + setUI() setLayout() } @@ -33,10 +34,13 @@ class StatisticsView: UIView { // MARK: - Methods extension StatisticsView { + private func setUI() { + totalImage.do { $0.image = .icSNS } + titleLabel.do { $0.text = I18N.total $0.font = .Pretendard(.regular, size: 14) @@ -45,6 +49,7 @@ extension StatisticsView { $0.textAlignment = .center } } + private func setLayout() { addSubviews(totalImage, titleLabel)