-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
273 additions
and
0 deletions.
There are no files selected for viewing
113 changes: 113 additions & 0 deletions
113
Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePicker.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IndexPath> = [] // 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..<totalRows { | ||
for column in 1..<totalColumns { | ||
let time = self.timeHeaders[row - 1] // 시간 | ||
guard column - 1 < dates.count else { continue } | ||
let date = dates[column - 1] // 날짜 | ||
let combinedKey = "\(date)T\(time):00:00" | ||
let indexPath = IndexPath(item: (row * totalColumns) + column, section: 0) | ||
mapping[combinedKey] = indexPath | ||
} | ||
} | ||
return mapping | ||
} | ||
|
||
func reloadCellBackgrounds() { | ||
self.cellAvailability = calculateCellAvailability(totalRows: self.timeHeaders.count + 1, | ||
totalColumns: self.dateHeaders.count + 1, | ||
startTimes: mockMemberStartTime) | ||
self.reloadData() | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePickerCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// | ||
// SchedulePickerCell.swift | ||
// Noostak_iOS | ||
// | ||
// Created by 오연서 on 1/10/25. | ||
// | ||
|
||
import UIKit | ||
|
||
final class SchedulePickerCell: UICollectionViewCell { | ||
|
||
static let identifier = "SchedulePickerCell" | ||
|
||
private var textLabel = UILabel() | ||
var isSelectedCell: Bool = false { | ||
didSet { | ||
backgroundColor = isSelectedCell ? .appBlue400 : .appWhite | ||
} | ||
} | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setupCell() | ||
setUpHierarchy() | ||
setUpUI() | ||
setUpLayout() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
private func setUpHierarchy() { | ||
self.addSubview(textLabel) | ||
} | ||
|
||
private func setUpUI() { | ||
textLabel.do { | ||
$0.font = .PretendardStyle.c4_r.font | ||
$0.numberOfLines = 0 | ||
$0.textAlignment = .center | ||
$0.textColor = .appGray600 | ||
} | ||
} | ||
|
||
private func setUpLayout() { | ||
textLabel.snp.makeConstraints { | ||
$0.center.equalToSuperview() | ||
} | ||
} | ||
|
||
private func setupCell() { | ||
self.backgroundColor = .appWhite | ||
self.layer.borderWidth = 0.5 | ||
self.layer.borderColor = UIColor.appGray200.cgColor | ||
} | ||
|
||
func configureHeader(for indexPath: IndexPath, dateHeaders: [String], timeHeaders: [String]) { | ||
let totalRows = timeHeaders.count + 1 | ||
let totalColumns = dateHeaders.count + 1 | ||
let row = indexPath.item / totalColumns | ||
let column = indexPath.item % totalColumns | ||
|
||
let isTopLeft = indexPath.item == 0 | ||
let isTopRight = indexPath.item == totalColumns - 1 | ||
let isBottomLeft = indexPath.item == (totalRows - 1) * totalColumns | ||
let isBottomRight = indexPath.item == totalRows * totalColumns - 1 | ||
|
||
/// dateHeader, timeHeader text binding | ||
if row == 0, column > 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 | ||
} | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
Noostak_iOS/Noostak_iOS/Global/Components/SchedulePicker/SchedulePickerLayout.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |