From e4629bb8e9d4d5d4865ec43e8031cdf3d9c4363a Mon Sep 17 00:00:00 2001 From: luckyyy Date: Sun, 12 Jan 2025 20:06:43 +0900 Subject: [PATCH] =?UTF-8?q?[Feat/#25]=20Schedule=20Picker=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SchedulePicker/SchedulePicker.swift | 113 ++++++++++++++++++ .../SchedulePicker/SchedulePickerCell.swift | 96 +++++++++++++++ .../SchedulePicker/SchedulePickerLayout.swift | 64 ++++++++++ 3 files changed, 273 insertions(+) create mode 100644 Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePicker.swift create mode 100644 Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePickerCell.swift create mode 100644 Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePickerLayout.swift diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePicker.swift b/Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePicker.swift new file mode 100644 index 0000000..f1ab322 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePicker.swift @@ -0,0 +1,113 @@ +// +// SchedulePicker.swift +// Noostak_iOS +// +// Created by 오연서 on 1/10/25. +// + +import UIKit + +final class SchedulePicker: UICollectionView { + enum Mode { + case editMode + case readMode + } + + private let layout = SchedulePickerLayout() + private let timeHeaders: [String] + private let dateHeaders: [String] + private let mode: Mode + private var selectedCells: Set = [] // edit Mode + private var cellAvailability: [IndexPath: Int] = [:] // read Mode + + init(timeHeaders: [String], dateHeaders: [String], mode: Mode) { + self.timeHeaders = timeHeaders + self.dateHeaders = dateHeaders + self.mode = mode + super.init(frame: .zero, collectionViewLayout: layout) + self.layout.configure(totalRows: timeHeaders.count + 1, totalColumns: dateHeaders.count + 1) + self.register(SchedulePickerCell.self, forCellWithReuseIdentifier: SchedulePickerCell.identifier) + self.cellAvailability = calculateCellAvailability(totalRows: timeHeaders.count + 1, + totalColumns: dateHeaders.count + 1, + //Fix: mockMemberStartTime 변경 필요 + startTimes: mockMemberStartTime) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func addSelectedCell(at indexPath: IndexPath) { + guard mode == .editMode else { return } + if let cell = cellForItem(at: indexPath) as? SchedulePickerCell { + cell.isSelectedCell.toggle() + if cell.isSelectedCell { + selectedCells.insert(indexPath) + } else { + selectedCells.remove(indexPath) + } + } + } + + func configureCellBackground(_ cell: SchedulePickerCell, for indexPath: IndexPath, participants: Int) { + guard mode == .readMode else { return } + let count = cellAvailability[indexPath, default: 0] + let ratio = Float(count) / Float(participants) + + switch ratio { + case 0.01...0.2: + cell.backgroundColor = .appBlue50 + case 0.2...0.4: + cell.backgroundColor = .appBlue200 + case 0.4...0.6: + cell.backgroundColor = .appBlue400 + case 0.6...0.8: + cell.backgroundColor = .appBlue700 + case 0.8...1: + cell.backgroundColor = .appBlue800 + default: + cell.backgroundColor = .clear + } + } +} + +/// .ReadMode 색상 반환 로직 +extension SchedulePicker { + ///각 시간에 대한 가능 인원 계산 + private func calculateCellAvailability(totalRows: Int, totalColumns: Int, startTimes: [String]) -> [IndexPath: Int] { + var cellAvailability: [IndexPath: Int] = [:] + let dateTimeMapping = createDateTimeMapping(totalRows: totalRows, totalColumns: totalColumns) + for startTime in startTimes { + if let indexPath = dateTimeMapping[startTime] { + cellAvailability[indexPath, default: 0] += 1 + } + } + return cellAvailability + } + + /// 시각 - cell 매핑 + private func createDateTimeMapping(totalRows: Int, totalColumns: Int) -> [String: IndexPath] { + var mapping: [String: IndexPath] = [:] + //FIX: mockDateList 변경 필요 + let dates = mockDateList.map { String($0.prefix(10)) } + + for row in 1.. 0 { + self.textLabel.text = dateHeaders[column - 1] + } else if column == 0, row > 0 { + self.textLabel.text = "\(timeHeaders[row - 1])시" + } else { + self.textLabel.text = "" + } + + /// 테이블 모서리 둥글게 + if isTopLeft || isTopRight || isBottomLeft || isBottomRight { + self.layer.cornerRadius = 10 + self.layer.masksToBounds = true + self.layer.borderColor = UIColor.appGray200.cgColor + if isTopLeft { + self.layer.maskedCorners = [.layerMinXMinYCorner] + } else if isTopRight { + self.layer.maskedCorners = [.layerMaxXMinYCorner] + } else if isBottomLeft { + self.layer.maskedCorners = [.layerMinXMaxYCorner] + } else if isBottomRight { + self.layer.maskedCorners = [.layerMaxXMaxYCorner] + } + } else { + self.layer.cornerRadius = 0 + } + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePickerLayout.swift b/Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePickerLayout.swift new file mode 100644 index 0000000..6419f66 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePickerLayout.swift @@ -0,0 +1,64 @@ +// +// SchedulePickerLayout.swift +// Noostak_iOS +// +// Created by 오연서 on 1/10/25. +// + +import UIKit + +final class SchedulePickerLayout: UICollectionViewFlowLayout { + private let fixedFirstColumnWidth: CGFloat = 42 + private let fixedFirstRowHeight: CGFloat = 36 + + private var totalRows: Int = 0 + private var totalColumns: Int = 0 + private let minimumSpacing: CGFloat = 0 + + func configure(totalRows: Int = 0, totalColumns: Int = 0) { + self.totalRows = totalRows + self.totalColumns = totalColumns + invalidateLayout() + } + + override func prepare() { + super.prepare() + guard let collectionView = collectionView else { return } + let remainingWidth = collectionView.bounds.width - fixedFirstColumnWidth - CGFloat(totalColumns - 1) * minimumSpacing + let dynamicColumnWidth = remainingWidth / CGFloat(totalColumns - 1) + let dynamicRowHeight = 32.0 + + itemSize = CGSize(width: dynamicColumnWidth, height: dynamicRowHeight) + minimumLineSpacing = 0 + minimumInteritemSpacing = 0 + sectionInset = .zero + } + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + attributes?.forEach { layoutAttribute in + let indexPath = layoutAttribute.indexPath + let column = indexPath.item % totalColumns + let row = indexPath.item / totalColumns + + // 첫 번째 열의 너비 고정, 열 간 간격 조정 + if column == 0 { + layoutAttribute.frame.size.width = fixedFirstColumnWidth + layoutAttribute.frame.origin.x = 0 + } else { // 두 번째 열 이후 + let previousColumnRight = fixedFirstColumnWidth + CGFloat(column - 1) * (itemSize.width + minimumInteritemSpacing) + layoutAttribute.frame.origin.x = previousColumnRight + } + + // 첫 번째 행의 높이 고정, 행 간 간격 조정 + if indexPath.item < totalColumns { + layoutAttribute.frame.size.height = fixedFirstRowHeight + layoutAttribute.frame.origin.y = 0 + } else { // 두 번째 행 이후 + let previousRowBottom = fixedFirstRowHeight + CGFloat(row - 1) * (itemSize.height + minimumLineSpacing) + layoutAttribute.frame.origin.y = previousRowBottom + } + } + return attributes + } +}