[새싹 iOS] 11주차_FSCalendar + CustomCell

임승섭·2023년 10월 4일
1

새싹 iOS

목록 보기
25/44

이용한 라이브러리 : FSCalendar
https://github.com/WenchaoD/FSCalendar

완성본

  • 셀마다 뒷배경으로 이미지가 들어간다
  • 선택한 날짜의 이미지는 진하게 보여주고, 선택되지 않은 날짜의 이미지는 흐리게 보여준다
  • 기본적으로 있는 디폴트 타이틀뷰 대신, 원하는 레이블과 버튼들을 넣은 커스텀 타이틀뷰를 삽입한다
  • 앱스토어 링크

코드

Setting Calendar

  • calendar 인스턴스에 대해 기본적인 설정을 해준다
var calendar = FSCalendar()	// 인스턴스 생성


func settingCalendar() {
   
    // 커스텀 셀 등록 (CalendarCell 클래스는 밑에서 구현)
    calendar.register(CalendarCell.self, forCellReuseIdentifier: CalendarCell.description())
    
    
    // 프로토콜 연결
    calendar.delegate = self
    calendar.dataSource = self
    
    
    // 초기 날짜 지정
    calendar.setCurrentPage(Date(), animated: true)
    calendar.select(Date())
   
    
    // 기존의 헤더 가림 -> 커스텀 헤더뷰 디자인 예정
    calendar.appearance.headerTitleColor = .clear
    calendar.appearance.headerMinimumDissolvedAlpha = 0.0
    calendar.headerHeight = 66

    
    // 각종 설정
    calendar.today = nil	
    calendar.scrollDirection = .horizontal
    calendar.locale = Locale.init(identifier: "en")
    calendar.scope = .month
    calendar.translatesAutoresizingMaskIntoConstraints = false
    
    calendar.appearance.titleSelectionColor = .lightGray.withAlphaComponent(0.5)
    calendar.appearance.selectionColor = .clear
    calendar.appearance.weekdayFont = .boldSystemFont(ofSize: 14)
    calendar.appearance.caseOptions = .weekdayUsesSingleUpperCase
    calendar.appearance.weekdayTextColor = .black
    calendar.appearance.titleFont = .boldSystemFont(ofSize: 12)
    
    calendar.weekdayHeight = 10
    calendar.placeholderType = .none
}

Custom Cell

  • 고려했던 부분
    • 뒷배경 이미지의 사이즈
      • Calendar의 height에 따라 셀의 높이와 너비가 달라지기 때문에,
        여분을 남기기 위해 둘 중 작은 크기를 기준으로 잡았다
    • titleLabel의 위치
      • 날짜가 표시된 레이블이 셀 기준 기본적으로 약간 위에 위치해있기 때문에 레이아웃을 다시 잡아준다.
  • 두 가지 고려사항 때문에 시간을 꽤 오래 썼는데, 막상 최종 코드는 너무 간단해서 당황스럽다..
class CalendarCell: FSCalendarCell {
    
    // 뒤에 표시될 이미지
    var backImageView = {
        let view = UIImageView()
        view.contentMode = .scaleAspectFill
        view.clipsToBounds = true
        return view
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        // 날짜 텍스트가 디폴트로 약간 위로 올라가 있어서, 아예 레이아웃을 잡아준다
        self.titleLabel.snp.makeConstraints { make in
            make.center.equalTo(contentView)
        }
        
        contentView.insertSubview(backImageView, at: 0)
        backImageView.snp.makeConstraints { make in
            make.center.equalTo(contentView)
            make.size.equalTo(minSize())
        }
        backImageView.layer.cornerRadius = minSize()/2
    }
    
    required init(coder aDecoder: NSCoder!) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        
        backImageView.image = nil
    }
    
    // 셀의 높이와 너비 중 작은 값을 리턴한다
    func minSize() -> CGFloat {
        let width = contentView.bounds.width - 5
        let height = contentView.bounds.height - 5

        return (width > height) ? height : width
    }
}

Delegate, DataSource

extension MonthCalendarViewController: FSCalendarDelegate, FSCalendarDataSource {
    
    // 오늘 이후의 날짜는 선택이 불가능하다
    func maximumDate(for calendar: FSCalendar) -> Date {
        return Date()
    }

    
    func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell {
  
        guard let cell = calendar.dequeueReusableCell(withIdentifier: CalendarCell.description(), for: date, at: position) as? CalendarCell else { return FSCalendarCell() }
        
        // (viewModel) url을 받아서, 배경 이미지를 띄워준다
        viewModel.fetchArtwork(date) { url in
            cell.backImageView.kf.setImage(with: url)
            print(date)
        }
        
        // 현재 선택되어 있는 날짜인지 확인 후 배경 이미지의 alpha값을 조절한다
        cell.backImageView.alpha = viewModel.isCurrentSelected(date) ? 1 : 0.5

        return cell
    }
    
    
    func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {

        // (viewModel) 현재 선택된 날짜 업데이트
        viewModel.updateSelectedDate(date)
        
        // 기존에 선택했던 날짜의 셀 배경의 alpha값을 다시 0.5로 바꿔주고,
        // 새롭게 선택한 날짜의 셀 배경의 alpha값을 1로 바꿔준다
		if let previousCell = calendar.cell(for: viewModel.previousSelectedDate.value, at: monthPosition) as? CalendarCell {
            previousCell.backImageView.alpha = 0.5
        }
        if let currentCell = calendar.cell(for: viewModel.currentSelectedDate.value, at: monthPosition) as? CalendarCell {
            currentCell.backImageView.alpha = 1
        }
    }


    func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
    	// 캘린더를 스와이프해서 이전/다음 달로 넘길 때, 
        // 타이틀뷰에 있는 레이블 값을 바꿔준다 (October)
        currentPageDate = calendar.currentPage
        monthView.headerLabel.text = Constant.DateFormat.headerDateFormatter.string(from: currentPageDate)
        calendar.reloadData()
    }
}

이슈(Large Title)

  • navigationController에 large title이 세팅되어있고,
    viewWillAppear에서 calendar.reloadData()를 실행시키면
    navigation 부분이 망가지는 이슈가 발생한다

정상 (reload 제거했을 때)

비정상 (reload 했을 때)

2개의 댓글

comment-user-thumbnail
2023년 11월 15일

꿀팁공유해주셔서 감사해요
이미지 넣는거 참고하고 갑니다!!
복받으세요

답글 달기
comment-user-thumbnail
2024년 2월 23일

안녕하세요:) 새싹 4기인데, 반가워서 댓글 남겨요!
FSCalendar 커스텀하는 코드 잘 보고갑니다🥹잘 정리해주셔서 너무 도움되네요!

답글 달기