본문 바로가기

iOS/3rd Party Library

[iOS] FSCalendar 라이브러리

 

 

GitHub - WenchaoD/FSCalendar: A fully customizable iOS calendar library, compatible with Objective-C and Swift

A fully customizable iOS calendar library, compatible with Objective-C and Swift - WenchaoD/FSCalendar

github.com

 

Objective-C 및 Swift와 호환되는 커스텀이 가능한 캘린더 라이브러리

 

FSCalendar Cheat sheet


 

주의사항

  1. FSCalendarView의 appearance로 설정한 값들은 FSCalendarDelegateAppearance의 메서드보다 우선순위가 낮습니다.
  2. FSCalendarDelegateAppearance 메서드를 사용하는 경우 채택하고 있는지 확인

 

예제코드 스크린샷

 

import UIKit
import FSCalendar

final class ViewController: UIViewController {

    // MARK: - UI
    private lazy var calendarWrapView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(calendar)
        return view
    }()

    private lazy var calendar: FSCalendar = {
        let calendar = FSCalendar()
        calendar.translatesAutoresizingMaskIntoConstraints = false
        return calendar
    }()

    private var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"

        return formatter
    }()

    private var today = Date()
    private lazy var eightDaysLater = Calendar.current.date(byAdding: .day, value: +8, to: self.today)

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        self.view.addSubview(calendarWrapView)

        NSLayoutConstraint.activate([
            calendarWrapView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            calendarWrapView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            calendarWrapView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            calendarWrapView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),

            calendar.topAnchor.constraint(equalTo: calendarWrapView.topAnchor),
            calendar.leadingAnchor.constraint(equalTo: calendarWrapView.leadingAnchor),
            calendar.trailingAnchor.constraint(equalTo: calendarWrapView.trailingAnchor),
            calendar.bottomAnchor.constraint(equalTo: calendarWrapView.bottomAnchor)
        ])

        self.configureCalendar()
    }

    private func configureCalendar() {
        // MARK: - 기본 설정

        // FSCalendarDelegate 설정
        self.calendar.delegate = self

        // FSCalendarDataSource 설정
        self.calendar.dataSource = self

        // Locale 설정 - 🚨 한국으로 설정시 자동으로 상단요일 텍스트가 일~월로 표시됨
        self.calendar.locale = Locale(identifier: "ko_KR")

        // cornerRadius
        self.calendar.layer.cornerRadius = 8

        // 달력 스크린 가능여부
        self.calendar.scrollEnabled = true

        // 달력 스크린 방향
        self.calendar.scrollDirection = .horizontal

        // 달력 범위 - .week(주간), .month(월간)
        self.calendar.scope = .month

        // MARK: - 상단 요일

        // 상단 요일 - 폰트, 사이즈 변경 - 🚨 텍스트변경이후에 호출하면 Default영어로 변경됨
        self.calendar.appearance.weekdayFont = UIFont.systemFont(ofSize: 20, weight: .semibold)

        // 상단 요일 - 텍스트 변경
        /*
        self.calendar.calendarWeekdayView.weekdayLabels[0].text = "일"
        self.calendar.calendarWeekdayView.weekdayLabels[1].text = "월"
        self.calendar.calendarWeekdayView.weekdayLabels[2].text = "화"
        self.calendar.calendarWeekdayView.weekdayLabels[3].text = "수"
        self.calendar.calendarWeekdayView.weekdayLabels[4].text = "목"
        self.calendar.calendarWeekdayView.weekdayLabels[5].text = "금"
        self.calendar.calendarWeekdayView.weekdayLabels[6].text = "토"
        */

        // 상단 요일 - 요약옵션 - 🚨 영어인 경우 동작함 (.weekdayUsesUpperCase: 대문자3글자, headerUsesCapitalized, headerUsesUpperCase: 첫글자 대문자인 3글자, weekdayUsesSingleUpperCase: 한글자)
        // self.calendar.appearance.caseOptions = .headerUsesCapitalized

        // 상단 요일 - 글자색
        self.calendar.appearance.weekdayTextColor = .systemYellow

        // MARK: - 숫자

        // 숫자 - 폰트, 사이즈 변경
        self.calendar.appearance.titleFont = UIFont.systemFont(ofSize: 20, weight: .semibold)

        // 숫자 - 해당 월이 아닌 날짜의 색 지정
        self.calendar.appearance.titlePlaceholderColor = .systemBlue
        
        // 숫자 - 해당 월이 아닌 날짜 표시 옵션 - (fillSixRows: 6줄, fillHeadTail: 보이는 기준, none: 제거)
        self.calendar.placeholderType = .fillSixRows

        // 숫자 - 해당 월인 날짜 색
        self.calendar.appearance.titleDefaultColor = .systemGreen

        // 숫자 - 현재 날짜 원 색상 - 🚨 현재 날짜가 선택된 경우는 선택한 원 색상의 우선순위가 높음
        self.calendar.appearance.todayColor = .red

        // 숫자 - 현재 날짜 텍스트 색상 - 🚨 현재 날짜가 선택된 경우는 선택한 텍스트 색상의 우선순위가 높음
        self.calendar.appearance.titleTodayColor = .blue

        // 숫자 - 선택한 원 색상
        self.calendar.appearance.selectionColor = .black

        // 숫자 - 선택한 텍스트 색상
        self.calendar.appearance.titleSelectionColor = .green

        // 숫자 - subtitle간의 간격 조정
        self.calendar.appearance.subtitleOffset = CGPoint(x: 4, y: 2)

        // MARK: - 헤더

        // Header - DateFormat
        self.calendar.appearance.headerDateFormat = "yy년 MM월"

        // Header - 높이
        self.calendar.headerHeight = 60

        // Header - 양 옆 년도 월 투명도 - 🚨 스크롤방향이 horizontal인 경우만 표시됨
        self.calendar.appearance.headerMinimumDissolvedAlpha = 1.0
        
        // Header Title - 폰트
        self.calendar.appearance.headerTitleFont = .systemFont(ofSize: 10, weight: .light)

        // Header Title - 색상
        self.calendar.appearance.headerTitleColor = .systemRed

        // Header Title - 방향
        self.calendar.appearance.headerTitleAlignment = .center

        // MARK: - Etc
        // 캘린더 선택 처리
        self.calendar.select(self.today)

        // 이벤트 - 기본 색상
        self.calendar.appearance.eventDefaultColor = UIColor.green

        // 이벤트 - 선택 색상
        self.calendar.appearance.eventSelectionColor = UIColor.red

        // 이벤트 - 간격
        self.calendar.appearance.eventOffset = CGPoint(x: 10, y: -7)

        // 다중 선택 가능여부
        self.calendar.allowsMultipleSelection = true

        // Swipe로 선택 가능여부
        self.calendar.swipeToChooseGesture.isEnabled = false
    }

}

// MARK: - FSCalendarDelegateAppearance, FSCalendarDataSource (FSCalendarDelegateAppearance가 FSCalendarDelegate를 채택 중
extension ViewController: FSCalendarDelegateAppearance, FSCalendarDataSource {

    // 날짜를 선택했을 때 호출
    func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
        print("📅 :\\(date) 날짜 선택")
    }

    // 선택된 날짜를 선택했을 때 호출
    func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
        print("📅 :\\(date) 날짜 선택 해제")
    }

    // 날짜 선택 직전 호출
    func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool {
        // 3개 까지는 선택 됨
        return (calendar.selectedDates.count > 2) ? false : true
    }

    // 선택된 날짜의 채워진 색상 지정
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, fillSelectionColorFor date: Date) -> UIColor? {
        return UIColor.systemIndigo
    }

    // 선택된 날짜 테두리 색상
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, borderSelectionColorFor date: Date) -> UIColor? {
        return UIColor.systemPink
    }

    // 모든 날짜의 채워진 색상 지정
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, fillDefaultColorFor date: Date) -> UIColor? {
        return UIColor.purple.withAlphaComponent(0.1)
    }

    // title의 디폴트 색상 - 🚨 사용시 titleDefaultColor와 titlePlaceholderColor 무시됨
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, titleDefaultColorFor date: Date) -> UIColor? {
        let components = Calendar.current.dateComponents([.weekday], from: date)

        print("date : \\(date), today : \\(self.today)" )

        let isMinimumTarget = (Calendar.current.compare(date, to: self.today, toGranularity: .day) != .orderedAscending)
        let isMaximumTarget = (Calendar.current.compare(date, to: self.eightDaysLater!, toGranularity: .day) != .orderedDescending)
        let isTarget = isMinimumTarget && isMaximumTarget

        switch components.weekday {
        case 1:
            return .blue.withAlphaComponent(isTarget ? 1.0 : 0.4)
        case 7:
            return .red.withAlphaComponent(isTarget ? 1.0 : 0.4)
        default:
            return .black.withAlphaComponent(isTarget ? 1.0 : 0.4)
        }
    }

    // subtitle의 디폴트 색상
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, subtitleDefaultColorFor date: Date) -> UIColor? {
        return UIColor.brown
    }

    // 원하는 날짜에 subtitle 지정
    func calendar(_ calendar: FSCalendar, subtitleFor date: Date) -> String? {
        let calendarDateString = dateFormatter.string(from: date)
        let todayDateString = dateFormatter.string(from: Date())

        if calendarDateString == todayDateString {
            return "오늘"
        } else {
            return "sub"
        }
    }

    // 원하는 날짜에 title 지정
    func calendar(_ calendar: FSCalendar, titleFor date: Date) -> String? {
        let calendarDateString = dateFormatter.string(from: date)
        let todayDateString = dateFormatter.string(from: Date())

        if calendarDateString == todayDateString {
            return "오늘"
        } else {
            return nil
        }
    }

    // 이벤트 표시 갯수 - 최대 3
    func calendar(_ calendar: FSCalendar, numberOfEventsFor date: Date) -> Int {
        return 2
    }
    
    // 선택 가능한 최소 날짜
    func minimumDate(for calendar: FSCalendar) -> Date {
        guard let eightDaysAgo = Calendar.current.date(byAdding: .day, value: -8, to: self.today)
        else { return Date()}

        return eightDaysAgo
    }

    // 선택 가능한 최대 날짜
    func maximumDate(for calendar: FSCalendar) -> Date {
        guard let eightDaysLater = Calendar.current.date(byAdding: .day, value: +8, to: self.today)
        else { return Date()}

        return eightDaysLater
    }

    // 페이지 변화에 대한 이벤트 - 스크롤
    func calendarCurrentPageDidChange(_ calendar: FSCalendar) {
        print("페이지 변환")
    }

    // 이벤트 위치
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, eventOffsetFor date: Date) -> CGPoint {
        return CGPoint(x: -7, y: -50)
    }

    // 기본 이벤트 색상 - 배열 순서대로 이벤트 색상 설정
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, eventDefaultColorsFor date: Date) -> [UIColor]? {
        return [UIColor.red]
    }

    // 선택된 이벤트 색상 - 배열 순서대로 이벤트 색상 설정
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, eventSelectionColorsFor date: Date) -> [UIColor]? {
        return [UIColor.green, UIColor.blue]
    }

}