Skip to content

Commit

Permalink
Merge pull request #78 from boostcampwm-2022/feature/CalendarView
Browse files Browse the repository at this point in the history
Feature/calendar view
  • Loading branch information
jinwoong16 committed Dec 6, 2022
2 parents 60a0424 + 0df45f0 commit 45ecf32
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 40 deletions.
8 changes: 8 additions & 0 deletions DailyQuest/DailyQuest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@
A5AC96E629223F06003B7637 /* QuestsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AC96E529223F06003B7637 /* QuestsStorage.swift */; };
A5AC96E829223F27003B7637 /* RealmQuestsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AC96E729223F27003B7637 /* RealmQuestsStorage.swift */; };
B50078D629222F3F0070AFC4 /* CircleCheckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50078D529222F3F0070AFC4 /* CircleCheckView.swift */; };
B5115453292CD07100FDBD22 /* CalendarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5115452292CD07100FDBD22 /* CalendarViewModel.swift */; };
B5833F732924C08900503E0D /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5833F722924C08900503E0D /* CalendarView.swift */; };
B58DFC0A29227DA800C68A4B /* CalendarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58DFC0929227DA800C68A4B /* CalendarCell.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -291,6 +293,8 @@
A5AC96E529223F06003B7637 /* QuestsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestsStorage.swift; sourceTree = "<group>"; };
A5AC96E729223F27003B7637 /* RealmQuestsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmQuestsStorage.swift; sourceTree = "<group>"; };
B50078D529222F3F0070AFC4 /* CircleCheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleCheckView.swift; sourceTree = "<group>"; };
B5115452292CD07100FDBD22 /* CalendarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewModel.swift; sourceTree = "<group>"; };
B5833F722924C08900503E0D /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
B58DFC0929227DA800C68A4B /* CalendarCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarCell.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -530,6 +534,7 @@
B50078D529222F3F0070AFC4 /* CircleCheckView.swift */,
3499551F29234637007AB99E /* CustomProgressBar.swift */,
34113BEC2934BD3D00AB4919 /* TextFieldForm.swift */,
B5833F722924C08900503E0D /* CalendarView.swift */,
);
path = Common;
sourceTree = "<group>";
Expand Down Expand Up @@ -685,6 +690,7 @@
isa = PBXGroup;
children = (
34EE6EB82924CAA1005AF583 /* QuestViewModel.swift */,
B5115452292CD07100FDBD22 /* CalendarViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
Expand Down Expand Up @@ -1156,6 +1162,7 @@
34874AA229250C43000570DF /* UIButton+.swift in Sources */,
A50F9A3F292679BC005C00FE /* NetworkConfigure.swift in Sources */,
34A529E7292481E1001BAD34 /* BrowseCoordinator.swift in Sources */,
B5115453292CD07100FDBD22 /* CalendarViewModel.swift in Sources */,
34A529D329247903001BAD34 /* TabCoordinator.swift in Sources */,
3417B1462935DA9D00900454 /* DefaultBrowseUseCase.swift in Sources */,
347D258D292C6E220038FCA2 /* MessageBubble.swift in Sources */,
Expand All @@ -1167,6 +1174,7 @@
3449AD5D2922197000B87619 /* User.swift in Sources */,
9BD8CCF52935C38300E6EA2F /* UserDTO+Mapping.swift in Sources */,
A51F01CD29233ABB0031ECA2 /* RealmUserInfoStorage.swift in Sources */,
B5833F732924C08900503E0D /* CalendarView.swift in Sources */,
A50F9A3429266F45005C00FE /* NetworkService.swift in Sources */,
A51F01DA292345990031ECA2 /* BrowseQuest.swift in Sources */,
A5AC96E629223F06003B7637 /* QuestsStorage.swift in Sources */,
Expand Down
227 changes: 227 additions & 0 deletions DailyQuest/DailyQuest/Presentation/Common/CalendarView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
//
// CalendarView.swift
// DailyQuest
//
// Created by wickedRun on 2022/11/15.
//

import UIKit
import SnapKit

class CalendarView: UIView {

lazy var yearMonthLabel: UILabel = {
let view = UILabel()
view.adjustsFontSizeToFitWidth = true
view.font = .systemFont(ofSize: 32, weight: .bold)
view.text = dateFormatter.string(from: currentDay)
return view
}()

lazy var weekdayLabels: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.distribution = .fillEqually
var calendar = Calendar.current
calendar.locale = .init(identifier: "ko_KR")

calendar.shortWeekdaySymbols.forEach {
let label = UILabel()
label.adjustsFontSizeToFitWidth = true
label.text = $0
label.textAlignment = .center
view.addArrangedSubview(label)
}

return view
}()

lazy var monthCollectionView: UICollectionView = {
let layout = setupCollectionViewLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.showsHorizontalScrollIndicator = false
collectionView.showsVerticalScrollIndicator = false
collectionView.bounces = false
collectionView.isPagingEnabled = true
collectionView.register(CalendarCell.self, forCellWithReuseIdentifier: CalendarCell.reuseIdentifier)
collectionView.delegate = self
return collectionView
}()

private let dateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy년 MM월"
return formatter
}()

private var currentDay: Date {
didSet {
yearMonthLabel.text = dateFormatter.string(from: currentDay)
}
}

var dataSource: UICollectionViewDiffableDataSource<Int, DisplayDate>!

var itemsBySection: [[CalendarView.DisplayDate]] = [[], [], []]

override init(frame: CGRect = .zero) {
self.currentDay = Date.now

super.init(frame: frame)
self.itemsBySection = self.setupMonths()

addSubviews()
setupConstraints()
setupDataSource()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func draw(_ rect: CGRect) {
super.draw(rect)

monthCollectionView.scrollToItem(at: IndexPath(item: 0, section: 1), at: .centeredHorizontally, animated: false)
}

private func addSubviews() {
addSubview(yearMonthLabel)
addSubview(weekdayLabels)
addSubview(monthCollectionView)
}

private func setupConstraints() {
yearMonthLabel.snp.makeConstraints { make in
make.top.equalToSuperview().inset(5)
make.horizontalEdges.equalToSuperview().inset(20)
}

weekdayLabels.snp.makeConstraints { make in
make.top.equalTo(yearMonthLabel.snp.bottom).offset(10)
make.horizontalEdges.equalToSuperview().inset(5)
}

monthCollectionView.snp.makeConstraints { make in
make.top.equalTo(weekdayLabels.snp.bottom).offset(10)
make.bottom.horizontalEdges.equalToSuperview().inset(5)
}
}

private func setupCollectionViewLayout() -> UICollectionViewLayout {
let itemWidth: CGFloat = 1 / 7
let groupHeight: CGFloat = 1 / 6

let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(itemWidth),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(groupHeight)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 7)

let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging

let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal

let layout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)

return layout
}

private func setupDataSource() {
self.dataSource = UICollectionViewDiffableDataSource(collectionView: monthCollectionView) { collectionView, indexPath, item in
guard
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarCell.reuseIdentifier, for: indexPath) as? CalendarCell
else {
preconditionFailure()
}

cell.configure(state: item.state, day: item.date.day)

return cell
}

var snapshot = NSDiffableDataSourceSnapshot<Int, DisplayDate>()
itemsBySection.indices.forEach { index in
snapshot.appendSections([index])
snapshot.appendItems(itemsBySection[index], toSection: index)
}
dataSource.apply(snapshot)
}

private func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Int, DisplayDate>()
let allSectionIndex = itemsBySection.indices.map { Int($0) }
snapshot.appendSections(allSectionIndex)
allSectionIndex.forEach { index in
snapshot.appendItems(itemsBySection[index], toSection: index)
}
dataSource.apply(snapshot, animatingDifferences: false)
}
}

extension CalendarView: UICollectionViewDelegate {

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard let indexPath = monthCollectionView.indexPathsForVisibleItems.first else {
return
}

if indexPath.section > 1 {
nextMonth()
} else if indexPath.section < 1 {
lastMonth()
} else {
return
}

applySnapshot()
monthCollectionView.scrollToItem(at: IndexPath(item: 0, section: 1), at: .centeredHorizontally, animated: false)
}

private func fetchDisplayDaysOfMonth(for date: Date?) -> [DisplayDate] {
guard let date else { return [] }

return date.rangeFromStartWeekdayOfLastMonthToEndDayOfCurrentMonth.map { DisplayDate(date: $0, state: .none) } + date.rangeDaysOfMonth.map { DisplayDate(date: $0, state: .normal) }
}

private func setupMonths() -> [[DisplayDate]] {
let startDayOfPrevMonth = currentDay.startDayOfLastMonth
let startDayOfNextMonth = currentDay.startDayOfNextMonth

return [startDayOfPrevMonth, currentDay, startDayOfNextMonth].map(fetchDisplayDaysOfMonth(for:))
}

private func nextMonth() {
currentDay = currentDay.nextMonthOfCurrentDay!
let monthAfterNext = currentDay.nextMonthOfCurrentDay!
let monthAfterNextDisplayDays = fetchDisplayDaysOfMonth(for: monthAfterNext)

self.itemsBySection.removeFirst()
self.itemsBySection.append(monthAfterNextDisplayDays)
}

private func lastMonth() {
currentDay = currentDay.lastMonthOfCurrentDay!
let monthBeforeLast = currentDay.lastMonthOfCurrentDay!
let monthBeforeLastDisplayDays = fetchDisplayDaysOfMonth(for: monthBeforeLast)

self.itemsBySection.removeLast()
self.itemsBySection.insert(monthBeforeLastDisplayDays, at: 0)
}
}

extension CalendarView {

struct DisplayDate: Hashable {
let date: Date
let state: CalendarCell.State
}
}
39 changes: 33 additions & 6 deletions DailyQuest/DailyQuest/Presentation/Common/Cells/CalendarCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import UIKit
import SnapKit

class CalendarCell: UICollectionViewCell {
final class CalendarCell: UICollectionViewCell {

/// 재사용 식별자
static let reuseIdentifier = "CalendarCell"

// MARK: - Sub Views

Expand Down Expand Up @@ -45,13 +48,16 @@ class CalendarCell: UICollectionViewCell {

private func setupContstraints() {
circleCheckView.snp.makeConstraints { make in
make.top.horizontalEdges.equalToSuperview()
make.height.equalTo(circleCheckView.snp.width)
make.top.equalToSuperview().inset(5)
make.centerX.equalToSuperview().priority(.high)
make.width.equalTo(self.snp.width).multipliedBy(0.9).inset(5)
make.height.equalTo(circleCheckView.snp.width).priority(.required)
}

dayLabel.snp.makeConstraints { make in
make.top.equalTo(circleCheckView.snp.bottom).offset(4)
make.bottom.horizontalEdges.equalToSuperview()
make.horizontalEdges.equalToSuperview()
make.bottom.lessThanOrEqualToSuperview().priority(.high)
}
}

Expand All @@ -61,8 +67,29 @@ class CalendarCell: UICollectionViewCell {
/// - parameters:
/// - state : CircleCheckView.State
/// - day : Int
func configure(state: CircleCheckView.State, day: Int) {
func configure(state: CalendarCell.State, day: Int) {
self.isHidden = false

switch state {
case .none:
self.isHidden = true
case .normal:
self.circleCheckView.setNormal()
case .display(let number):
self.circleCheckView.setNumber(to: number)
case .done:
self.circleCheckView.setDone()
}
dayLabel.text = "\(day)"
circleCheckView.updateState(state)
}
}

extension CalendarCell {

enum State: Hashable {
case none
case normal
case display(Int)
case done
}
}
Loading

0 comments on commit 45ecf32

Please sign in to comment.