Skip to content

Commit

Permalink
[Feat/#25] Schedule Picker 작성
Browse files Browse the repository at this point in the history
  • Loading branch information
oyslucy committed Jan 12, 2025
1 parent 4b30dcb commit e4629bb
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 0 deletions.
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()
}
}
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
}
}
}
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
}
}

0 comments on commit e4629bb

Please sign in to comment.