-
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.
Merge pull request #29 from Noostak/feat/NST-51/schedulePicker
[Feat/NST-51] #25 Schedule picker 컴포넌트 작성
- Loading branch information
Showing
4 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
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,116 @@ | ||
// | ||
// 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) | ||
setupLayout() | ||
setupFoundation() | ||
} | ||
|
||
private func setupLayout() { | ||
layout.configure(totalRows: timeHeaders.count + 1, totalColumns: dateHeaders.count + 1) | ||
} | ||
|
||
private func setupFoundation() { | ||
self.register(SchedulePickerCell.self, forCellWithReuseIdentifier: SchedulePickerCell.identifier) | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
// MARK: Public Methods | ||
func addSelectedCell(at indexPath: IndexPath) { | ||
guard mode == .editMode, | ||
let cell = cellForItem(at: indexPath) as? SchedulePickerCell | ||
else { return } | ||
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) | ||
cell.backgroundColor = calculateBackgroundColor(for: ratio) | ||
} | ||
|
||
func updateCellAvailability(with dateList: [String], startTimes: [String]) { | ||
guard mode == .readMode else { return } | ||
self.cellAvailability = calculateCellAvailability(totalRows: self.timeHeaders.count + 1, | ||
totalColumns: self.dateHeaders.count + 1, | ||
dateList: dateList, | ||
startTimes: startTimes) | ||
reloadData() | ||
} | ||
} | ||
|
||
// MARK: Internal Logics | ||
extension SchedulePicker { | ||
///각 시간에 대한 가능 인원 계산 | ||
private func calculateCellAvailability(totalRows: Int, totalColumns: Int, dateList: [String], startTimes: [String]) -> [IndexPath: Int] { | ||
var cellAvailability: [IndexPath: Int] = [:] | ||
let dateTimeMapping = createDateTimeMapping(totalRows: totalRows, totalColumns: totalColumns, dateList: dateList) | ||
for startTime in startTimes { | ||
if let indexPath = dateTimeMapping[startTime] { | ||
cellAvailability[indexPath, default: 0] += 1 | ||
} | ||
} | ||
return cellAvailability | ||
} | ||
|
||
/// 시각 - cell 매핑 | ||
private func createDateTimeMapping(totalRows: Int, totalColumns: Int, dateList: [String]) -> [String: IndexPath] { | ||
var mapping: [String: IndexPath] = [:] | ||
let dates = dateList.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 | ||
} | ||
|
||
private func calculateBackgroundColor(for ratio: Float) -> UIColor { | ||
switch ratio { | ||
case 0.01...0.2: return .appBlue50 | ||
case 0.2...0.4: return .appBlue200 | ||
case 0.4...0.6: return .appBlue400 | ||
case 0.6...0.8: return .appBlue700 | ||
case 0.8...1: return .appBlue800 | ||
default: return .clear | ||
} | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
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,101 @@ | ||
// | ||
// 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 totalColumns = dateHeaders.count + 1 | ||
let row = indexPath.item / totalColumns | ||
let column = indexPath.item % totalColumns | ||
|
||
/// 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 = "" | ||
} | ||
} | ||
|
||
func configureTableRoundness(for indexPath: IndexPath, dateHeaders: [String], timeHeaders: [String]) { | ||
let totalRows = timeHeaders.count + 1 | ||
let totalColumns = dateHeaders.count + 1 | ||
|
||
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 | ||
|
||
if isTopLeft || isTopRight || isBottomLeft || isBottomRight { | ||
self.layer.cornerRadius = 10 | ||
self.layer.masksToBounds = true | ||
switch true { | ||
case isTopLeft: | ||
self.layer.maskedCorners = [.layerMinXMinYCorner] | ||
case isTopRight: | ||
self.layer.maskedCorners = [.layerMaxXMinYCorner] | ||
case isBottomLeft: | ||
self.layer.maskedCorners = [.layerMinXMaxYCorner] | ||
case isBottomRight: | ||
self.layer.maskedCorners = [.layerMaxXMaxYCorner] | ||
default: | ||
break | ||
} | ||
} else { | ||
self.layer.cornerRadius = 0 | ||
} | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
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,65 @@ | ||
// | ||
// SchedulePickerLayout.swift | ||
// Noostak_iOS | ||
// | ||
// Created by 오연서 on 1/10/25. | ||
// | ||
|
||
import UIKit | ||
|
||
extension SchedulePicker { | ||
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 | ||
} | ||
|
||
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 | ||
} | ||
} | ||
} |
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