diff --git a/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj b/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj index a1e037d8..f97d4d82 100644 --- a/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj +++ b/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 09A1465F2A192C4900DDC308 /* WeekMissionResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A1465E2A192C4900DDC308 /* WeekMissionResponseDTO.swift */; }; 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 */; }; 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 */; }; @@ -202,6 +203,7 @@ 099FC98829B3233D005B37E6 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -451,6 +453,7 @@ isa = PBXGroup; children = ( 3B027A9D299C34DA00BEB65C /* HomeViewController.swift */, + 09CF56032B09F23800526C8C /* HomeDataSource.swift */, 092C09B42A484DD900E9B06B /* HomeDeleteViewController.swift */, 0930DE6129B80550007958DE /* MissionDetailViewController.swift */, 09582B4E29BEBAFA00EF3207 /* DetailCalendarViewController.swift */, @@ -1276,6 +1279,7 @@ 09022D4629C44BC300DE6E49 /* MissionCalendarCell.swift in Sources */, 6CA2083A2A195906001C4247 /* AuthResponseDTO.swift in Sources */, 09582B4B29BDE37C00EF3207 /* DetailFooterReusableView.swift in Sources */, + 09CF56042B09F23800526C8C /* HomeDataSource.swift in Sources */, 093DB03F2A15FCC100ECA5F6 /* MissionDetailResponseDTO.swift in Sources */, 6CF4705B29A68EA9008D145C /* URLConstant.swift in Sources */, 3BD3B5C829B8F82C00D3575B /* AddMissionTextFieldView.swift in Sources */, diff --git a/iOS-NOTTODO/iOS-NOTTODO/Network/API/Home/HomeAPI.swift b/iOS-NOTTODO/iOS-NOTTODO/Network/API/Home/HomeAPI.swift index 240ca4e8..bbd68b99 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Network/API/Home/HomeAPI.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Network/API/Home/HomeAPI.swift @@ -20,37 +20,44 @@ final class HomeAPI { public private(set) var missionDailyData: GeneralArrayResponse? public private(set) var missionDetailDailyData: GeneralResponse? public private(set) var updateMissionStatus: GeneralResponse? - public private(set) var missionWeekly: GeneralResponse? + public private(set) var missionWeekly: GeneralArrayResponse? public private(set) var addAnotherDay: GeneralResponse? public private(set) var particularDays: GeneralArrayResponse? // MARK: - GET - func getDailyMission(date: String, completion: @escaping (NetworkResult) -> Void) { - homeProvider.request(.dailyMission(date: date)) { response in - switch response { - case let .success(response): - let statusCode = response.statusCode - let data = response.data - let networkResult = NetworkBase.judgeStatus(by: statusCode, data, [DailyMissionResponseDTO].self) - completion(networkResult) - case let .failure(err): - print(err) + func getDailyMission(date: String, completion: @escaping (GeneralArrayResponse?) -> Void) { + homeProvider.request(.dailyMission(date: date)) { result in + switch result { + case .success(let response): + do { + self.missionDailyData = try response.map(GeneralArrayResponse?.self) + guard let missionDailtData = self.missionDailyData else { return completion(nil) } + completion(missionDailtData) + } catch let err { + print(err.localizedDescription, 500) + } + case .failure(let err): + print(err.localizedDescription) + completion(nil) } } } - func getWeeklyMissoin(startDate: String, completion: @escaping (NetworkResult) -> Void) { - homeProvider.request(.missionWeekly(startDate: startDate)) { response in - switch response { - case let .success(response): - let statusCode = response.statusCode - let data = response.data - let networkResult = NetworkBase.judgeStatus(by: statusCode, data, - [WeekMissionResponseDTO].self) - completion(networkResult) - case let .failure(err): - print(err) + func getWeeklyMissoin(startDate: String, completion: @escaping (GeneralArrayResponse?) -> Void) { + homeProvider.request(.missionWeekly(startDate: startDate)) { result in + switch result { + case .success(let response): + do { + self.missionWeekly = try response.map(GeneralArrayResponse?.self) + guard let missionWeekly = self.missionWeekly else { return completion(nil) } + completion(missionWeekly) + } catch let err { + print(err.localizedDescription, 500) + } + case .failure(let err): + print(err.localizedDescription) + completion(nil) } } } @@ -117,7 +124,7 @@ final class HomeAPI { case .success(let response): do { self.updateMissionStatus = try response.map(GeneralResponse?.self) - guard self.updateMissionStatus != nil else { return } + guard self.updateMissionStatus != nil else { return completion(nil) } completion(self.updateMissionStatus) } catch let err { print(err.localizedDescription, 500) @@ -137,7 +144,7 @@ final class HomeAPI { case .success(let response): do { self.addAnotherDay = try response.map(GeneralResponse?.self) - guard let addAnotherDay = self.addAnotherDay else { return } + guard let addAnotherDay = self.addAnotherDay else { return completion(nil) } completion(addAnotherDay) } catch let err { print(err.localizedDescription, 500) diff --git a/iOS-NOTTODO/iOS-NOTTODO/Network/DataModel/Home/DailyMissionResponseDTO.swift b/iOS-NOTTODO/iOS-NOTTODO/Network/DataModel/Home/DailyMissionResponseDTO.swift index 1805e850..01d2cb30 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Network/DataModel/Home/DailyMissionResponseDTO.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Network/DataModel/Home/DailyMissionResponseDTO.swift @@ -14,8 +14,18 @@ enum CompletionStatus: String, Codable, Hashable { // MARK: - DailyMissionResponseDTO struct DailyMissionResponseDTO: Codable, Hashable { - var id: Int - var title: String - var situationName: String - var completionStatus: CompletionStatus + + var uuid = UUID() + let id: Int + let title: String + let situationName: String + let completionStatus: CompletionStatus + + enum CodingKeys: String, CodingKey { + case id, title, situationName, completionStatus + } + + static func == (lhs: DailyMissionResponseDTO, rhs: DailyMissionResponseDTO) -> Bool { + lhs.uuid == rhs.uuid + } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/AchievementViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/AchievementViewController.swift index 234bf429..5b10ab65 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/AchievementViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/AchievementViewController.swift @@ -39,7 +39,7 @@ final class AchievementViewController: UIViewController { AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Achieve.viewAccomplish) if let today = monthCalendar.calendar.today { - monthCalendar.yearMonthLabel.text = Utils.dateFormatterString(format: I18N.yearMonthTitle, date: today) + monthCalendar.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, date: today)) monthCalendar.calendar.currentPage = today requestMonthAPI(month: Utils.dateFormatterString(format: "yyyy-MM", date: today)) } @@ -136,7 +136,7 @@ extension AchievementViewController { extension AchievementViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { func calendarCurrentPageDidChange(_ calendar: FSCalendar) { self.currentPage = calendar.currentPage - monthCalendar.yearMonthLabel.text = Utils.dateFormatterString(format: I18N.yearMonthTitle, date: calendar.currentPage) + monthCalendar.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, date: calendar.currentPage)) reloadMonthData(month: Utils.dateFormatterString(format: "yyyy-MM", date: calendar.currentPage)) } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift index 924729f4..56cb9f3d 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift @@ -146,22 +146,14 @@ extension DetailAchievementViewController { } extension DetailAchievementViewController { - func requestDetailAPI(date: String) { - HomeAPI.shared.getDailyMission(date: date) { [self] result in - switch result { - case let .success(data): - guard let data = data as? [DailyMissionResponseDTO] else { return } - self.missionList = data - updateData(item: missionList) - case .requestErr: - print("requestErr") - case .pathErr: - print("pathErr") - case .serverErr: - print("serverErr") - case .networkFail: - print("networkFail") - } + + 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) } } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/AddMission/Cells/DateCollectionViewCell.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/AddMission/Cells/DateCollectionViewCell.swift index 509f44c5..dab64b92 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/AddMission/Cells/DateCollectionViewCell.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/AddMission/Cells/DateCollectionViewCell.swift @@ -222,7 +222,7 @@ extension DateCollectionViewCell { extension DateCollectionViewCell: FSCalendarDelegate, FSCalendarDelegateAppearance { func calendarCurrentPageDidChange(_ calendar: FSCalendar) { - calendarView.yearMonthLabel.text = Utils.dateFormatterString(format: I18N.yearMonthTitle, date: calendar.currentPage) + calendarView.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, date: calendar.currentPage)) } func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool { diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Calendar/CalendarView.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Calendar/CalendarView.swift index 6645ba41..5958ecfb 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Calendar/CalendarView.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Calendar/CalendarView.swift @@ -11,18 +11,27 @@ import FSCalendar import Then import SnapKit +protocol CalendarViewDelegate: AnyObject { + + func todayBtnTapped() +} + final class CalendarView: UIView { + // MARK: - Properties + + let today = Date() + var monthCalendarClosure: ((_ month: String) -> Void)? + weak var delegate: CalendarViewDelegate? + // MARK: - UI Components - - let yearMonthLabel = UILabel() + + private let yearMonthLabel = UILabel() let todayButton = UIButton(configuration: .filled()) - let horizonStackView = UIStackView() - let leftButton = UIButton() - let rightButton = UIButton() + private let horizonStackView = UIStackView() + private let leftButton = UIButton() + private let rightButton = UIButton() var calendar = WeekMonthFSCalendar() - private lazy var today: Date = { return Date() }() - var monthCalendarClosure: ((_ month: String) -> Void)? // MARK: - Life Cycle @@ -43,10 +52,12 @@ final class CalendarView: UIView { extension CalendarView { private func setCalendar(scope: FSCalendarScope, scrollDirection: FSCalendarScrollDirection) { + calendar = WeekMonthFSCalendar(calendarScope: scope, scrollDirection: scrollDirection) } private func setUI() { + backgroundColor = .ntdBlack yearMonthLabel.do { @@ -56,15 +67,23 @@ extension CalendarView { } todayButton.do { - $0.configuration?.image = .icBackToday - $0.configuration?.title = I18N.todayButton - $0.configuration?.imagePadding = 2 - $0.configuration?.contentInsets = NSDirectionalEdgeInsets.init(top: 3, leading: 6, bottom: 2, trailing: 7) - $0.configuration?.cornerStyle = .capsule - $0.configuration?.attributedTitle?.font = .Pretendard(.regular, size: 14) - $0.configuration?.baseBackgroundColor = .gray2 - $0.configuration?.baseForegroundColor = .gray5 + var config = UIButton.Configuration.filled() + config.image = .icBackToday + config.title = I18N.todayButton + config.imagePadding = 2 + config.cornerStyle = .capsule + config.attributedTitle?.font = .Pretendard(.regular, size: 14) + config.baseBackgroundColor = .gray2 + config.baseForegroundColor = .gray5 + config.contentInsets = NSDirectionalEdgeInsets.init(top: 3, + leading: 6, + bottom: 2, + trailing: 7) + + $0.configuration = config + $0.addTarget(self, action: #selector(todayBtnTapped), for: .touchUpInside) } + horizonStackView.do { $0.axis = .horizontal $0.spacing = 16 @@ -79,9 +98,15 @@ extension CalendarView { $0.setImage(.calendarRight, for: .normal) $0.addTarget(self, action: #selector(nextBtnTapped), for: .touchUpInside) } + + calendar.do { + $0.collectionView.register(MissionCalendarCell.self, + forCellWithReuseIdentifier: MissionCalendarCell.identifier) + } } private func setLayout(scope: FSCalendarScope) { + switch scope { case .week: addSubviews(calendar, yearMonthLabel, todayButton) @@ -102,16 +127,15 @@ extension CalendarView { $0.directionalHorizontalEdges.equalToSuperview().inset(11) $0.bottom.equalToSuperview().inset(20) } + case .month: addSubviews(horizonStackView, calendar) horizonStackView.addArrangedSubviews(leftButton, yearMonthLabel, rightButton) - - leftButton.snp.makeConstraints { - $0.size.equalTo(CGSize(width: 25, height: 25)) - } - rightButton.snp.makeConstraints { - $0.size.equalTo(CGSize(width: 25, height: 25)) + [leftButton, rightButton].forEach { + $0.snp.makeConstraints { + $0.size.equalTo(CGSize(width: 25, height: 25)) + } } horizonStackView.snp.makeConstraints { @@ -131,6 +155,7 @@ extension CalendarView { } func scrollCurrentPage(calendar: WeekMonthFSCalendar, isPrev: Bool) { + let gregorian = Calendar(identifier: .gregorian) calendar.setCurrentPage( gregorian.date(byAdding: calendar.scope == .week ? .weekOfMonth : .month, value: isPrev ? -1 : 1, to: calendar.currentPage)!, animated: true) let monthDateFormatter = DateFormatter() @@ -143,7 +168,7 @@ extension CalendarView { // MARK: - Action extension CalendarView { - + @objc func prevBtnTapped(_sender: UIButton) { scrollCurrentPage(calendar: calendar, isPrev: true) @@ -155,6 +180,15 @@ extension CalendarView { } } +extension CalendarView { + + @objc + func todayBtnTapped(_sender: UIButton) { + delegate?.todayBtnTapped() + } + +} + extension CalendarView { func setCalendarSelectedDate(_ dates: [Date]) { @@ -184,5 +218,4 @@ extension CalendarView { $0.bottom.equalToSuperview().inset(45) } } - } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift index bf78e680..761869f6 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift @@ -9,13 +9,18 @@ import UIKit final class CompositionalLayout { - class func _vertical(_ itemWidth: NSCollectionLayoutDimension, _ itemHeight: NSCollectionLayoutDimension, _ groupWidth: NSCollectionLayoutDimension, _ groupHeight: NSCollectionLayoutDimension, count: Int, edge: NSDirectionalEdgeInsets?) -> NSCollectionLayoutSection { + class func vertical(itemWidth: NSCollectionLayoutDimension = .fractionalWidth(1), + itemHeight: NSCollectionLayoutDimension = .fractionalWidth(1), + groupWidth: NSCollectionLayoutDimension = .fractionalWidth(1), + groupHeight: NSCollectionLayoutDimension = .fractionalWidth(1), + count: Int, + edge: NSDirectionalEdgeInsets = .zero) -> NSCollectionLayoutSection { let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: itemWidth, heightDimension: itemHeight)) - let group = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: groupWidth, heightDimension: groupHeight), subitem: item, count: count ) - return section(group, edge ?? NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: groupWidth, heightDimension: groupHeight), subitem: item, count: count) + return createSection(group, edge) } - class func section(_ group: NSCollectionLayoutGroup, _ edge: NSDirectionalEdgeInsets) -> NSCollectionLayoutSection { + class func createSection(_ group: NSCollectionLayoutGroup, _ edge: NSDirectionalEdgeInsets) -> NSCollectionLayoutSection { let section = NSCollectionLayoutSection(group: group) section.contentInsets = edge return section diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/HomeEmptyCollectionViewCell.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/HomeEmptyCollectionViewCell.swift index c02504f8..04764571 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/HomeEmptyCollectionViewCell.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/HomeEmptyCollectionViewCell.swift @@ -21,6 +21,8 @@ final class HomeEmptyCollectionViewCell: UICollectionViewCell { private let logoImage = UIImageView() private let emptyLabel = UILabel() + // MARK: - Life Cycle + override init(frame: CGRect) { super.init(frame: .zero) setUI() @@ -35,6 +37,7 @@ final class HomeEmptyCollectionViewCell: UICollectionViewCell { // MARK: - Methods extension HomeEmptyCollectionViewCell { + private func setUI() { backgroundColor = .clear diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/MissionListCollectionViewCell.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/MissionListCollectionViewCell.swift index b6eb4015..6063f709 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/MissionListCollectionViewCell.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/Cells/MissionListCollectionViewCell.swift @@ -16,13 +16,13 @@ final class MissionListCollectionViewCell: UICollectionViewCell { static let identifier = "MissionListCollectionViewCell" + var userId: Int = 0 var isTappedClosure: ((_ result: Bool, _ userId: Int) -> Void)? var isTapped: Bool = false { didSet { setUI() } } - var userId: Int = 0 // MARK: - UI Components @@ -121,12 +121,16 @@ extension MissionListCollectionViewCell { func configure(model: DailyMissionResponseDTO) { self.userId = model.id + tagLabel.text = model.situationName missionLabel.text = model.title + missionLabel.lineBreakMode = .byTruncatingTail + switch model.completionStatus { case .UNCHECKED: isTapped = false case .CHECKED: isTapped = true } + checkButton.isSelected = isTapped } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/DetailCalendarViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/DetailCalendarViewController.swift index d21f78dd..0091f8d6 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/DetailCalendarViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/DetailCalendarViewController.swift @@ -114,19 +114,19 @@ extension DetailCalendarViewController { $0.trailing.equalToSuperview().inset(16) $0.size.equalTo(CGSize(width: 44, height: 35)) } - + monthCalendar.snp.makeConstraints { $0.centerY.equalTo(safeArea) $0.directionalHorizontalEdges.equalTo(safeArea).inset(15) $0.height.equalTo((getDeviceWidth()-30)*1.2) } - - monthCalendar.updateDetailConstraints() - + subLabel.snp.makeConstraints { $0.bottom.equalToSuperview().inset(25) $0.left.equalToSuperview().offset(17) } + + monthCalendar.updateConstraints() } } @@ -149,8 +149,7 @@ extension DetailCalendarViewController { extension DetailCalendarViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { func calendarCurrentPageDidChange(_ calendar: FSCalendar) { - monthCalendar.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, - date: calendar.currentPage)) + monthCalendar.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, date: calendar.currentPage)) guard let id = self.userId else { return } requestParticualrDatesAPI(id: id) } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeDataSource.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeDataSource.swift new file mode 100644 index 00000000..2b68d6ed --- /dev/null +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeDataSource.swift @@ -0,0 +1,215 @@ +// +// HomeDataSource.swift +// iOS-NOTTODO +// +// Created by JEONGEUN KIM on 11/19/23. +// + +import UIKit + +protocol HomeModalDelegate: AnyObject { + + func updateMissionStatus(id: Int, status: CompletionStatus) + func modifyMission(id: Int, type: MissionType) + func deleteMission(index: Int, id: Int) +} + +final class HomeDataSource { + + // MARK: - Properties + + typealias CellRegistration = UICollectionView.CellRegistration + typealias DataSource = UICollectionViewDiffableDataSource + typealias SnapShot = NSDiffableDataSourceSnapshot + + enum Sections: Int, Hashable { + + case mission, empty + } + + enum Item: Hashable { + + case mission(DailyMissionResponseDTO) + case empty + } + + private var currentSection: [Sections] = [.empty] + private var missionList: [DailyMissionResponseDTO] + + var dataSource: DataSource? + weak var modalDelegate: HomeModalDelegate? + + // MARK: - UI Components + + private let collectionView: UICollectionView + + init(collectionView: UICollectionView, missionList: [DailyMissionResponseDTO]) { + self.collectionView = collectionView + self.missionList = missionList + + setCollectionView() + setDataSource() + setSnapShot() + } + + private func setCollectionView() { + + collectionView.collectionViewLayout = createLayout() + } + + private func setDataSource() { + + let cellRegistration = createMissionCellRegistration() + let emptyRegistration = createEmptyCellRegistration() + + dataSource = DataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in + + switch item { + case .mission: + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, + for: indexPath, + item: item) + case .empty: + return collectionView.dequeueConfiguredReusableCell(using: emptyRegistration, + for: indexPath, + item: item) + } + }) + } + + private func createMissionCellRegistration() -> CellRegistration { + + return CellRegistration { cell, _, item in + guard let missionItem = self.getMissionItem(from: item) else { return } + + cell.configure(model: missionItem) + + cell.isTappedClosure = { [weak self] result, id in + guard let self else { return } + + let status = result ? CompletionStatus.UNCHECKED : CompletionStatus.CHECKED + self.modalDelegate?.updateMissionStatus(id: id, status: status) + + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.completeCheckMission(title: missionItem.title, situation: missionItem.situationName)) + } + } + } + + private func createEmptyCellRegistration() -> CellRegistration { + return CellRegistration { _, _, _ in } + } + + private func setSnapShot() { + + var snapshot = SnapShot() + + defer { + dataSource?.apply(snapshot, animatingDifferences: false) + } + + snapshot.appendSections([.empty]) + snapshot.appendItems([.empty], toSection: .empty) + } + + func updateSnapShot(missionList: [DailyMissionResponseDTO]) { + + self.missionList = missionList + guard var snapshot = dataSource?.snapshot() else { return } + + let newSections: [Sections] = self.missionList.isEmpty ? [.empty] : [.mission] + let item: [Item] = self.missionList.isEmpty ? [.empty] : missionList.map { .mission($0) } + + snapshot.deleteSections(currentSection) + snapshot.appendSections(newSections) + snapshot.appendItems(item, toSection: newSections.first) + + currentSection = newSections + dataSource?.apply(snapshot) + } + + private func createLayout() -> UICollectionViewLayout { + + let layout = UICollectionViewCompositionalLayout { sectionIndex, env in + + guard let section = self.dataSource?.snapshot().sectionIdentifiers[sectionIndex] else { return nil } + + switch section { + case .mission: + return self.missionSection(env: env) + case .empty: + return CompositionalLayout.vertical(count: 1, edge: .init(top: 30, leading: 0, bottom: 0, trailing: 0)) + } + } + return layout + } + + private func missionSection(env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { + + var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + config.backgroundColor = .clear + config.showsSeparators = false + config.trailingSwipeActionsConfigurationProvider = self.makeSwipeActions + + let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: env) + section.orthogonalScrollingBehavior = .none + section.interGroupSpacing = 18 + section.contentInsets = NSDirectionalEdgeInsets(top: 32, leading: 0, bottom: 0, trailing: 18) + + return section + } + + private func makeSwipeActions(for indexPath: IndexPath?) -> UISwipeActionsConfiguration? { + + guard let index = indexPath?.item, + let result = findMissionItem(with: missionList[index].uuid) else { return nil } + + let indexPath = result.index + let data = result.mission + + let deleteAction = UIContextualAction(style: .normal, title: "") { [unowned self] _, _, completion in + + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Detail.clickDeleteMission(section: "home", + title: data.title, + situation: data.situationName, + goal: "", + action: [])) + + self.modalDelegate?.deleteMission(index: indexPath, id: data.id) + completion(true) + } + + let modifyAction = UIContextualAction(style: .normal, title: "") { _, _, completionHandler in + + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Detail.clickEditMission(section: "home")) + + self.modalDelegate?.modifyMission(id: data.id, type: .update) + completionHandler(true) + } + + deleteAction.backgroundColor = .ntdRed + modifyAction.backgroundColor = .ntdBlue + + deleteAction.image = .icTrash + modifyAction.image = .icFix + + let swipeConfiguration = UISwipeActionsConfiguration(actions: [deleteAction, modifyAction]) + swipeConfiguration.performsFirstActionWithFullSwipe = false + + return swipeConfiguration + } + + private func getMissionItem(from item: Item) -> DailyMissionResponseDTO? { + if case let .mission(missionItem) = item { + return missionItem + } + return nil + } + + private func findMissionItem(with id: UUID) -> (index: Int, mission: DailyMissionResponseDTO)? { + guard let index = missionList.firstIndex(where: { $0.uuid == id }) else { + return nil + } + return (index, missionList[index]) + } + +} diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeDeleteViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeDeleteViewController.swift index f3e835a1..92f94bf1 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeDeleteViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeDeleteViewController.swift @@ -14,15 +14,19 @@ final class HomeDeleteViewController: UIViewController { // MARK: - Properties - private lazy var safeArea = self.view.safeAreaLayoutGuide var deleteClosure: (() -> Void)? + private lazy var safeArea = self.view.safeAreaLayoutGuide + // MARK: - UI Components private let deleteModalView = DeleteModalView() + // MARK: - Life Cycle + override func viewDidLoad() { super.viewDidLoad() + setUI() setLayout() } @@ -32,7 +36,7 @@ final class HomeDeleteViewController: UIViewController { let touch = touches.first! let location = touch.location(in: self.view) - if !self.view.frame.contains(location) { + if !view.frame.contains(location) { dismiss(animated: true) } } @@ -47,7 +51,7 @@ extension HomeDeleteViewController { deleteModalView.do { $0.deleteClosure = { self.deleteClosure?() - self.dismiss(animated: true) + self.dismiss(animated: true) } $0.cancelClosure = { self.dismiss(animated: true) diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift index 8d75533a..4e2755b0 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift @@ -16,21 +16,19 @@ final class HomeViewController: UIViewController { // MARK: - Properties private var missionList: [DailyMissionResponseDTO] = [] - private lazy var today: Date = { return Date() }() - private var selectedDate: Date? // 눌렀을 떄 date - dailymissionAPI 호출 시 사용 - private var current: Date? // 스와이프했을 때 일요일 date 구하기 위함 - weeklyAPI 호출 시 사용 - private var count: Int? private var calendarDataSource: [String: Float] = [:] + + private let today = Date() + private var selectedDate: Date? + private var current: Date? + private lazy var safeArea = self.view.safeAreaLayoutGuide - - enum Sections: Int, Hashable { - case mission, empty - } - var dataSource: UICollectionViewDiffableDataSource! = nil // MARK: - UI Components - private lazy var missionCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout()) + private var missionCollectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()) + private lazy var missionDataSource = HomeDataSource(collectionView: missionCollectionView, missionList: missionList) + private let weekCalendar = CalendarView(calendarScope: .week, scrollDirection: .horizontal) private let addButton = UIButton() @@ -39,16 +37,17 @@ final class HomeViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.viewHome) + dailyLoadData() weeklyLoadData() } override func viewDidLoad() { super.viewDidLoad() + setUI() - register() setLayout() - setupDataSource() + } } @@ -56,53 +55,34 @@ final class HomeViewController: UIViewController { extension HomeViewController { - private func dailyLoadData() { - let todayString = Utils.dateFormatterString(format: nil, date: self.selectedDate ?? today) - requestDailyMissionAPI(date: todayString) - } - - private func weeklyLoadData() { - let sunday = getSunday(date: self.current ?? today) - requestWeeklyMissoinAPI(startDate: Utils.dateFormatterString(format: nil, date: sunday)) - } - - func getSunday(date: Date) -> Date { - let cal = Calendar.current - var comps = cal.dateComponents([.weekOfYear, .yearForWeekOfYear], from: date) - comps.weekday = 1 - let sundayInWeek = cal.date(from: comps)! - return sundayInWeek - } - - private func register() { - missionCollectionView.register(MissionListCollectionViewCell.self, forCellWithReuseIdentifier: MissionListCollectionViewCell.identifier) - missionCollectionView.register(HomeEmptyCollectionViewCell.self, forCellWithReuseIdentifier: HomeEmptyCollectionViewCell.identifier) - } - private func setUI() { + view.backgroundColor = .ntdBlack weekCalendar.do { - $0.calendar.delegate = self - $0.calendar.dataSource = self - $0.calendar.register(MissionCalendarCell.self, forCellReuseIdentifier: MissionCalendarCell.identifier) - $0.todayButton.addTarget(self, action: #selector(todayBtnTapped), for: .touchUpInside) + $0.configure(delegate: self, datasource: self) + $0.delegate = self } missionCollectionView.do { $0.backgroundColor = .bg - $0.bounces = false $0.autoresizingMask = [.flexibleWidth, .flexibleHeight] + $0.bounces = false $0.delegate = self + $0.dataSource = missionDataSource.dataSource } addButton.do { $0.setImage(.addMission, for: .normal) $0.addTarget(self, action: #selector(addBtnTapped), for: .touchUpInside) } + + missionDataSource.modalDelegate = self + } private func setLayout() { + view.addSubviews(weekCalendar, missionCollectionView, addButton) weekCalendar.calendar.select(today) @@ -111,196 +91,142 @@ extension HomeViewController { $0.directionalHorizontalEdges.equalTo(safeArea) $0.height.equalTo(172) } + missionCollectionView.snp.makeConstraints { $0.top.equalTo(weekCalendar.snp.bottom) $0.directionalHorizontalEdges.equalTo(safeArea) $0.bottom.equalToSuperview() } + addButton.snp.makeConstraints { $0.width.height.equalTo(convertByHeightRatio(60)) $0.trailing.equalTo(safeArea).inset(18) $0.bottom.equalTo(safeArea).inset(20) } } +} + +// MARK: - Action + +extension HomeViewController: CalendarViewDelegate { - private func setupDataSource() { - dataSource = UICollectionViewDiffableDataSource(collectionView: missionCollectionView, cellProvider: { collectionView, indexPath, item in - let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section] - switch section { - case .mission: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCollectionViewCell.identifier, for: indexPath) as! MissionListCollectionViewCell - cell.configure(model: item as! DailyMissionResponseDTO ) - cell.isTappedClosure = { [self] result, id in - if result { - self.requestPatchUpdateMissionAPI(id: id, status: CompletionStatus.UNCHECKED ) - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.completeCheckMission(title: self.missionList[indexPath.row].title, situation: self.missionList[indexPath.row].situationName)) - } else { - self.requestPatchUpdateMissionAPI(id: id, status: CompletionStatus.CHECKED ) - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.completeCheckMission(title: self.missionList[indexPath.row].title, situation: self.missionList[indexPath.row].situationName)) - } - } - return cell - case .empty: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeEmptyCollectionViewCell.identifier, for: indexPath) as! HomeEmptyCollectionViewCell - return cell - } - }) + @objc + func addBtnTapped(_sender: UIButton) { + + let nextViewController = RecommendViewController() + nextViewController.setSelectDate(Utils.dateFormatterString(format: "yyyy.MM.dd", date: selectedDate ?? Date())) + + Utils.push(navigationController, nextViewController) } - private func reloadData() { - var snapshot = NSDiffableDataSourceSnapshot() - defer { - dataSource.apply(snapshot, animatingDifferences: false) - } - snapshot.appendSections([.empty]) - snapshot.appendItems([0], toSection: .empty) - dataSource.apply(snapshot, animatingDifferences: true) + func todayBtnTapped() { + + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.clickReturnToday) + + weekCalendar.calendar.select(today) + weekCalendar.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, date: today)) + requestDailyMissionAPI(date: Utils.dateFormatterString(format: nil, date: today)) } - private func updateData() { - var snapshot = dataSource.snapshot() - if missionList.isEmpty { - if snapshot.sectionIdentifiers.contains(.mission) { - snapshot.deleteSections([.mission]) - snapshot.appendSections([.empty]) - snapshot.appendItems([0], toSection: .empty) - } else if snapshot.sectionIdentifiers.contains(.empty) { - - } else { - snapshot.appendSections([.empty]) - snapshot.appendItems([0], toSection: .empty) - } - } else { - if snapshot.sectionIdentifiers.contains(.empty) { - snapshot.deleteSections([.empty]) - snapshot.appendSections([.mission]) - snapshot.appendItems(missionList, toSection: .mission) - - } else if snapshot.sectionIdentifiers.contains(.mission) { - snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .mission)) - snapshot.appendItems(missionList, toSection: .mission) - } else { - snapshot.appendSections([.mission]) - snapshot.appendItems(missionList, toSection: .mission) - } - } - dataSource.apply(snapshot) - } +} - private func layout() -> UICollectionViewLayout { - let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvirnment in - let section = self.dataSource.snapshot().sectionIdentifiers[sectionIndex] - switch section { - case .mission: - var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) - config.backgroundColor = .clear - config.showsSeparators = false - config.trailingSwipeActionsConfigurationProvider = self.makeSwipeActions - - let layoutSection = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvirnment) - layoutSection.orthogonalScrollingBehavior = .none - layoutSection.interGroupSpacing = 18 - layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 32, leading: 0, bottom: 0, trailing: 18) - - return layoutSection - - case .empty: - return CompositionalLayout._vertical(.fractionalWidth(1), .fractionalWidth(1), .fractionalWidth(1), .fractionalWidth(1), count: 1, edge: .init(top: 30, leading: 0, bottom: 0, trailing: 0)) - } - } - return layout +// MARK: - HomeModalDelegate + +extension HomeViewController: HomeModalDelegate { + + func updateMissionStatus(id: Int, status: CompletionStatus) { + + self.requestPatchUpdateMissionAPI(id: id, status: status) } - private func makeSwipeActions(for indexPath: IndexPath?) -> UISwipeActionsConfiguration? { - let deleteAction = UIContextualAction(style: .normal, title: "") { [unowned self] _, _, completion in - - guard let index = indexPath?.item else { return } - - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Detail.clickDeleteMission(section: "home", title: self.missionList[index].title, situation: self.missionList[index].situationName, goal: "", action: [])) - - let modalViewController = HomeDeleteViewController() - modalViewController.modalPresentationStyle = .overFullScreen - modalViewController.modalTransitionStyle = .crossDissolve - modalViewController.deleteClosure = { - self.requestDeleteMission(index: index) - } - present(modalViewController, animated: false) - completion(true) - } + func modifyMission(id: Int, type: MissionType) { - let modifyAction = UIContextualAction(style: .normal, title: "") { _, _, completionHandler in - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Detail.clickEditMission(section: "home")) - - guard let index = indexPath?.item else { return } - let id = self.missionList[index].id - let updateMissionViewController = AddMissionViewController() - updateMissionViewController.setMissionId(id) - updateMissionViewController.setViewType(.update) - Utils.push(self.navigationController, updateMissionViewController) - completionHandler(true) + let updateMissionViewController = AddMissionViewController() + + updateMissionViewController.setMissionId(id) + updateMissionViewController.setViewType(type) + + Utils.push(self.navigationController, updateMissionViewController) + } + + func deleteMission(index: Int, id: Int) { + + let modalViewController = HomeDeleteViewController() + + modalViewController.modalPresentationStyle = .overFullScreen + modalViewController.modalTransitionStyle = .crossDissolve + + modalViewController.deleteClosure = { + self.requestDeleteMission(index: index, id: id) } - deleteAction.backgroundColor = .ntdRed - modifyAction.backgroundColor = .ntdBlue - deleteAction.image = .icTrash - modifyAction.image = .icFix - - let swipeConfiguration = UISwipeActionsConfiguration(actions: [deleteAction, modifyAction]) - swipeConfiguration.performsFirstActionWithFullSwipe = false - return swipeConfiguration + + present(modalViewController, animated: false) } } // MARK: - Collectionview Delegate extension HomeViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if !missionList.isEmpty { + let modalViewController = MissionDetailViewController() modalViewController.modalPresentationStyle = .overFullScreen modalViewController.userId = missionList[indexPath.item].id - + modalViewController.deleteClosure = { [weak self] in - self?.dailyLoadData() - self?.weeklyLoadData() - self?.updateData() + guard let self else { return } + + self.dailyLoadData() + self.weeklyLoadData() + self.missionDataSource.updateSnapShot(missionList: self.missionList) } + modalViewController.moveDateClosure = { [weak self] date in + guard let self else { return } + let modifiedDate: Date = date.toDate(withFormat: "YYYY.MM.dd") - self?.weekCalendar.calendar.select(modifiedDate) - self?.requestDailyMissionAPI(date: Utils.dateFormatterString(format: nil, date: modifiedDate)) + self.weekCalendar.calendar.select(modifiedDate) + self.requestDailyMissionAPI(date: Utils.dateFormatterString(format: nil, date: modifiedDate)) } + self.present(modalViewController, animated: true) } } } -extension HomeViewController { - - @objc - func addBtnTapped(_sender: UIButton) { - let nextViewController = RecommendViewController() - nextViewController.setSelectDate(Utils.dateFormatterString(format: "yyyy.MM.dd", date: selectedDate ?? Date())) - Utils.push(navigationController, nextViewController) - } - - @objc - func todayBtnTapped(_sender: UIButton) { - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.clickReturnToday) - - weekCalendar.calendar.select(today) - weekCalendar.yearMonthLabel.text = Utils.dateFormatterString(format: I18N.yearMonthTitle, date: today) - requestDailyMissionAPI(date: Utils.dateFormatterString(format: nil, date: today)) - } -} - // MARK: - FSCalendar Delegate, DataSource, DelegateAppearance extension HomeViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { + func calendarCurrentPageDidChange(_ calendar: FSCalendar) { - weekCalendar.yearMonthLabel.text = Utils.dateFormatterString(format: I18N.yearMonthTitle, date: calendar.currentPage) - self.current = calendar.currentPage - let sunday = getSunday(date: calendar.currentPage) - requestWeeklyMissoinAPI(startDate: Utils.dateFormatterString(format: nil, date: sunday)) + + updateCalendar(for: calendar.currentPage) + } + + func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { + + self.selectedDate = date + + weekCalendar.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, date: date)) + requestDailyMissionAPI(date: Utils.dateFormatterString(format: nil, date: date)) + + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.clickWeeklyDate(date: Utils.dateFormatterString(format: nil, date: date))) + } + + func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { + let cell = calendar.dequeueReusableCell(withIdentifier: MissionCalendarCell.identifier, for: date, at: position) as! MissionCalendarCell + + guard let percentage = getPercentage(for: date) else { return cell } + + switch percentage { + case 0.0: cell.configure(.none, .week) + case 1.0: cell.configure(.rateFull, .week) + default: cell.configure(.rateHalf, .week) + } + + return cell } func calendar(_ calendar: FSCalendar, titleFor date: Date) -> String? { @@ -311,49 +237,12 @@ extension HomeViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalend Utils.dateFormatterString(format: "dd", date: date) } - func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { - self.selectedDate = date - weekCalendar.yearMonthLabel.text = Utils.dateFormatterString(format: I18N.yearMonthTitle, date: date) - requestDailyMissionAPI(date: Utils.dateFormatterString(format: nil, date: date)) - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.clickWeeklyDate(date: Utils.dateFormatterString(format: nil, date: date))) - } - func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, subtitleSelectionColorFor date: Date) -> UIColor? { - guard let count = self.count else { return .white } - let dateString = Utils.dateFormatterString(format: nil, date: date) - if let percentage = self.calendarDataSource[dateString] { - switch (count, percentage) { - case (_, 1.0): return .black - default: return .white - } - } - return .white + return subtitleColorFor(date: date) } func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, subtitleDefaultColorFor date: Date) -> UIColor? { - guard let count = self.count else { return .white } - let dateString = Utils.dateFormatterString(format: nil, date: date) - if let percentage = self.calendarDataSource[dateString] { - switch (count, percentage) { - case (_, 1.0): return .black - default: return .white - } - } - return .white - } - - func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { - let cell = calendar.dequeueReusableCell(withIdentifier: MissionCalendarCell.identifier, for: date, at: position) as! MissionCalendarCell - guard let count = self.count else { return cell } - let dateString = Utils.dateFormatterString(format: nil, date: date) - if let percentage = self.calendarDataSource[dateString] { - switch (count, percentage) { - case (_, 1.0): cell.configure(.rateFull, .week) - case (_, 0.0): cell.configure(.none, .week) - case (2, 0.5), (3, 0.0..<1.0), (_, _): cell.configure(.rateHalf, .week) - } - } - return cell + return subtitleColorFor(date: date) } } @@ -362,65 +251,102 @@ extension HomeViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalend extension HomeViewController { func requestDailyMissionAPI(date: String) { - HomeAPI.shared.getDailyMission(date: date) { [weak self] result in - switch result { - case let .success(data): - guard let data = data as? [DailyMissionResponseDTO] else {return} - self?.missionList = [] - if !data.isEmpty { - self?.missionList = data - } - self?.updateData() - - case .pathErr: print("pathErr") - case .serverErr: print("serverErr") - case .networkFail: print("networkFail") - case .requestErr: print("networkFail") - } + + HomeAPI.shared.getDailyMission(date: date) { [weak self] response in + guard let self, let response = response, let data = response.data else { return } + + self.missionList = data + self.missionDataSource.updateSnapShot(missionList: data) } } private func requestWeeklyMissoinAPI(startDate: String) { - HomeAPI.shared.getWeeklyMissoin(startDate: startDate) { result in - switch result { - case let .success(data): - guard let data = data as? [WeekMissionResponseDTO] else { return } - self.calendarDataSource = [:] - for item in data { - self.calendarDataSource[item.actionDate] = item.percentage - self.count = self.calendarDataSource.count - } - self.weekCalendar.calendar.reloadData() - case .requestErr: print("requestErr") - case .pathErr: print("pathErr") - case .serverErr: print("serverErr") - case .networkFail: print("networkFail") - } + + HomeAPI.shared.getWeeklyMissoin(startDate: startDate) { [weak self] response in + guard let self, let response = response, let data = response.data else { return } + + let calendarData = data.compactMap { ($0.actionDate, $0.percentage) } + self.calendarDataSource = Dictionary(uniqueKeysWithValues: calendarData) + + self.weekCalendar.reloadCollectionView() } } private func requestPatchUpdateMissionAPI(id: Int, status: CompletionStatus) { - HomeAPI.shared.patchUpdateMissionStatus(id: id, status: status.rawValue) { [weak self] result in - guard let result = result else { return } - for index in 0..<(self?.missionList.count ?? 0) { - if self?.missionList[index].id == id { - guard let data = result.data else { return } - self?.missionList[index] = data - self?.weeklyLoadData() - self?.updateData() - } else {} + + HomeAPI.shared.patchUpdateMissionStatus(id: id, status: status.rawValue) { [weak self] response in + guard let self, let response = response, let data = response.data else { return } + + if let index = self.missionList.firstIndex(where: { $0.id == id }) { + self.missionList[index] = data + self.weeklyLoadData() + self.missionDataSource.updateSnapShot(missionList: self.missionList) } } } - private func requestDeleteMission(index: Int) { - let id = self.missionList[index].id - HomeAPI.shared.deleteMission(id: id) { [weak self] _ in - self?.dailyLoadData() - self?.weeklyLoadData() - self?.updateData() + private func requestDeleteMission(index: Int, id: Int) { + HomeAPI.shared.deleteMission(id: id) { [weak self] _ in + guard let self else { return } + + self.dailyLoadData() + self.weeklyLoadData() + self.missionDataSource.updateSnapShot(missionList: self.missionList) - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Detail.clickDeleteMission(section: "home", title: (self?.missionList[index].title)!, situation: (self?.missionList[index].situationName)!, goal: "", action: [])) + let data = self.missionList[index] + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Detail.clickDeleteMission(section: "home", + title: data.title, + situation: data.situationName, + goal: "", + action: [])) } } } + +// MARK: - Others + +extension HomeViewController { + + private func dailyLoadData() { + + let todayString = Utils.dateFormatterString(format: nil, date: self.selectedDate ?? today) + requestDailyMissionAPI(date: todayString) + } + + private func weeklyLoadData() { + + let sunday = getSunday(date: self.current ?? today) + requestWeeklyMissoinAPI(startDate: Utils.dateFormatterString(format: nil, date: sunday)) + } + + private func getSunday(date: Date) -> Date { + + let cal = Calendar.current + var comps = cal.dateComponents([.weekOfYear, .yearForWeekOfYear], from: date) + comps.weekday = 1 + let sundayInWeek = cal.date(from: comps)! + return sundayInWeek + } + + private func getPercentage(for date: Date) -> Float? { + + let dateString = Utils.dateFormatterString(format: nil, date: date) + return self.calendarDataSource[dateString] + } + + private func updateCalendar(for date: Date) { + + self.current = date + weekCalendar.configureYearMonth(to: Utils.dateFormatterString(format: I18N.yearMonthTitle, date: date)) + requestWeeklyMissoinAPI(startDate: Utils.dateFormatterString(format: nil, date: getSunday(date: date))) + } + + private func subtitleColorFor(date: Date) -> UIColor { + + if let percentage = getPercentage(for: date) { + return percentage == 1.0 ? .black : .white + } + + return .white + } +}