[새싹 iOS] 17주차_FSCalendar 날짜 범위(기간) 선택

임승섭·2023년 11월 12일
0

새싹 iOS

목록 보기
32/45

완성본


1. 소개

  • 구현 기능
    • 하루 선택 시 원 모양 배경으로 선택 날짜 표시
    • 시작/마지막 날짜 선택 시 두 날짜를 포함한 기간을 이어진 배경으로 표시
  • 단점
    • 셀 클릭 시마다 calendar.reloadData()를 실행해야 하기 때문에 리소스 낭비가 심하다.

2. 구현

(1). Calendar Setting

  • 사용할 calendar에 대해 기본적인 세팅을 한다

    var calendar = FSCalendar()
    
    calendar.scrollDirection = .vertical	// 스크롤 방향
    calendar.allowsMultipleSelection = true // 여러 날짜 선택 가능
    
    calendar.register(SelectDatesCustomCalendarCell.self, forCellReuseIdentifier: SelectDatesCustomCalendarCell.description())  // 커스텀 셀 등록
    
    calendar.appearance.titleFont = .boldSystemFont(ofSize: 18)	// 날짜 표시 레이블 폰트 설정
    calendar.appearance.headerTitleFont = .boldSystemFont(ofSize: 20)	// 월 표시 레이블 폰트 설정
    
    calendar.today = nil	// 기본 오늘 선택 해제
    calendar.appearance.selectionColor = .clear	// 기본 선택 배경 투명 -> 커스텀 셀 배경으로 표시
    calendar.appearance.caseOptions = .weekdayUsesSingleUpperCase  // 요일 텍스트를 영어 한글자로 표시
    
    // 기본 색상 선택
    calendar.appearance.titleDefaultColor = .black
    calendar.appearance.headerTitleColor = .black
    calendar.appearance.weekdayTextColor = .black
    calendar.appearance.titleSelectionColor = UIColor.appColor(.main1)

(2). enum

  • 편의를 위해 날짜 타입을 enum으로 정의해준다.
    enum SelectedDateType {
        case singleDate	// 날짜 하나만 선택된 경우 (원 모양 배경)
        case firstDate	// 여러 날짜 선택 시 맨 처음 날짜
        case middleDate // 여러 날짜 선택 시 맨 처음, 마지막을 제외한 중간 날짜
        case lastDate   // 여러 날짜 선택시 맨 마지막 날짜
        case notSelectd // 선택되지 않은 날짜
    }

(3). Custom Cell

  • 3개의 인스턴스가 필요하다
    • leftRectangle, rightRectangle, circle
    • 선택된 날짜의 타입(firstDate, middleDate, endDate)에 따라, 어떤 인스턴스를 Hidden 처리할지 결정한다.

레이아웃 및 기본 설정


class SelectDatesCustomCalendarCell: FSCalendarCell {

  var circleBackImageView = UIImageView()
  var leftRectBackImageView = UIImageView()
  var rightRectBackImageView = UIImageView()

  func setConfigure() {

      contentView.insertSubview(circleBackImageView, at: 0)
      contentView.insertSubview(leftRectBackImageView, at: 0)
      contentView.insertSubview(rightRectBackImageView, at: 0)
  }

  func setConstraints() {

      // 날짜 텍스트의 레이아웃을 센터로 잡아준다 (기본적으로 약간 위에 있다)
      self.titleLabel.snp.makeConstraints { make in
          make.center.equalTo(contentView)
      }

      leftRectBackImageView.snp.makeConstraints { make in
          make.leading.equalTo(contentView)
          make.trailing.equalTo(contentView.snp.centerX)
          make.height.equalTo(46)
          make.centerY.equalTo(contentView)
      }

      circleBackImageView.snp.makeConstraints { make in
          make.center.equalTo(contentView)
          make.size.equalTo(46)
      }

      rightRectBackImageView.snp.makeConstraints { make in
          make.leading.equalTo(contentView.snp.centerX)
          make.trailing.equalTo(contentView)
          make.height.equalTo(46)
          make.centerY.equalTo(contentView)
      }

  }

  func settingImageView() {
      circleBackImageView.clipsToBounds = true
      circleBackImageView.layer.cornerRadius = 23
		
      // 선택 날짜의 배경 색상을 여기서 정한다.
      [circleBackImageView, leftRectBackImageView, rightRectBackImageView].forEach { item  in
          item.backgroundColor = UIColor.appColor(.main3)
      }
  }
}

날짜의 타입에 따라 셀의 배경 정해주기

class SelectDatesCustomCalendarCell: FSCalendarCell {
    func updateBackImage(_ dateType: SelectedDateType) {
        switch dateType {
        case .singleDate:
            // left right hidden true
            // circle hidden false
            leftRectBackImageView.isHidden = true
            rightRectBackImageView.isHidden = true
            circleBackImageView.isHidden = false

        case .firstDate:
            // leftRect hidden true
            // circle, right hidden false
            leftRectBackImageView.isHidden = true
            circleBackImageView.isHidden = false
            rightRectBackImageView.isHidden = false

        case .middleDate:
            // circle hidden true
            // left, right hidden false
            circleBackImageView.isHidden = true
            leftRectBackImageView.isHidden = false
            rightRectBackImageView.isHidden = false

        case .lastDate:
            // rightRect hidden true
            // circle, left hidden false
            rightRectBackImageView.isHidden = true
            circleBackImageView.isHidden = false
            leftRectBackImageView.isHidden = false
        case .notSelectd:
            // all hidden
            circleBackImageView.isHidden = true
            leftRectBackImageView.isHidden = true
            rightRectBackImageView.isHidden = true
        }

    }

}

(4). ViewController

프로토콜 연결

class SelectedDateViewController: UIViewController {
    func settingCalendar() {
        mainView.calendar.delegate = self
        mainView.calendar.dataSource = self
    }
}

필요한 변수

class SelectedDateViewController: UIViewController {
	private var firstDate: Date?	// 배열 중 첫번째 날짜
    private var lastDate: Date?		// 배열 중 마지막 날짜
    private var datesRange: [Date] = []	// 선택된 날짜 배열
}

cellFor date

extension SelectedDateViewController: FSCalendarDataSource {
	
    // 매개변수로 들어온 date의 타입을 반환한다
    func typeOfDate(_ date: Date) -> SelectedDateType {
        
        let arr = datesRange
        
        if !arr.contains(date) {
            return .notSelectd	// 배열이 비어있으면 무조건 notSelected
        }
        
        else {
        	// 배열의 count가 1이고, firstDate라면 singleDate
            if arr.count == 1 && date == firstDate { return .singleDate }	
            
			// 배열의 count가 2 이상일 때, 각각 타입 반환
            if date == firstDate { return .firstDate }
            if date == lastDate { return .lastDate }
            
            else { return .middleDate }
        }
    }

    func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell {
        
        guard let cell = calendar.dequeueReusableCell(withIdentifier: SelectDatesCustomCalendarCell.description(), for: date, at: position) as? SelectDatesCustomCalendarCell else { return FSCalendarCell() }

		// 현재 그리는 셀의 date의 타입에 기반해서 셀 디자인
        cell.updateBackImage(typeOfDate(date))

        return cell
    }
}

didSelect

extension SelectedDateViewController: FSCalendarDelegate {
	func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
        
        // case 1. 현재 아무것도 선택되지 않은 경우 
        	// 선택 date -> firstDate 설정
        if firstDate == nil {
            firstDate = date
            datesRange = [firstDate!]
            
            mainView.calendar.reloadData() // (매번 reload)
            return
        }
        
        // case 2. 현재 firstDate 하나만 선택된 경우
        if firstDate != nil && lastDate == nil {
            // case 2 - 1. firstDate 이전 날짜 선택 -> firstDate 변경
            if date < firstDate! {
                calendar.deselect(firstDate!)
                firstDate = date
                datesRange = [firstDate!]
                
                mainView.calendar.reloadData()	// (매번 reload)
                return
            }
            
            // case 2 - 2. firstDate 이후 날짜 선택 -> 범위 선택
            else {
                var range: [Date] = []
                
                var currentDate = firstDate!
                while currentDate <= date {
                    range.append(currentDate)
                    currentDate = Calendar.current.date(byAdding: .day, value: 1, to: currentDate)!
                }
                
                for day in range {
                    calendar.select(day)
                }
                
                lastDate = range.last
                datesRange = range
                
                mainView.calendar.reloadData()	// (매번 reload)
                return
            }
        }
        
        // case 3. 두 개가 모두 선택되어 있는 상태 -> 현재 선택된 날짜 모두 해제 후 선택 날짜를 firstDate로 설정
        if firstDate != nil && lastDate != nil {

            for day in calendar.selectedDates {
                calendar.deselect(day)
            }
            
            lastDate = nil
            firstDate = date
            calendar.select(date)
            datesRange = [firstDate!]
                
            mainView.calendar.reloadData()	// (매번 reload)
            return
        }
        
        
    }
}

didDeselect

extension SelectDateViewController: FSCalendarDelegate {
    // 이미 선택된 날짜들 중 하나를 선택 -> 선택된 날짜 모두 초기화
    func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
    
        let arr = datesRange    
        if !arr.isEmpty {
            for day in arr {
                calendar.deselect(day)
            }
        }
        firstDate = nil
        lastDate = nil
        datesRange = []
        
        mainView.calendar.reloadData()	// (매번 reload)
    }

3. 완성

0개의 댓글