카테고리 없음

[Swift - UIKit] FSCalendar CustomCell 재사용 문제 - 완전해결

분홍이귀여워 2024. 7. 15. 21:13
반응형

 

저번 셀의 재사용 문제 포스팅을 했는데 완벽한 해결은 아니였어서 다른방법을 찾던중 해결을 하여 포스팅 합니다. 

 

문제

어떤 문제였는지는 저번포스팅을 참고해주세요~

2024.07.15 - [분류 전체보기] - [UIKit] FSCalendar CustomCell 사용시 재사용 문제

 

[UIKit] FSCalendar CustomCell 사용시 재사용 문제

FSCalendar 을 사용하여 TodoApp 을 만들고 있는데 문제가 생겼다.투두리스트 작성시 달력에는 해당날짜의 투두 체크 비율을 계산하여 얼마나 완료했는지 보기쉽게 그림으로 표시하고 있다.7월에만

boon-hong.tistory.com

 

 

포스팅이 실수로 삭제되어 다시 포스팅 하던 중 또다른 방법을 찾게 되었다.

다시 찾은방법(1번)을 기본적으로 해보고 그래도 안되면(2)번 방법, 그것도 안된다면 이전 포스팅 방법으로 해도 괜찮을거 같다. 

 

셀 재사용시 셀에 그림을 그릴때 prepareForReuse() 메서드나 분기처리로 초기화를 해줘도 안될경우 해결법 

1. CustomCell 에 배경색을 지정해주기

2. FSCalendar의 기본 Cell 을 등록해서 조건에 안맞을시  dequeueReusableCell 에 기본셀을 반환하는 방법

3. 배경과 같은색상으로 그림을 그려 안그린것처럼 보이게 하기(이전 포스팅 참고)

 

 

 

해결 ) 1번 방법 

CustomCell 에 배경색을 지정해주기

나는 처음 셀에 아무런 색상을 지정해주지 않았었다.

그 결과 셀의 배경이 검정색으로 나타났다.

 

 

그래서 셀의 백그라운드 컬러를 화이트로 해줬다.

그러면 이렇게 선이 보이게 된다.

 

 

여기서부터가 문제였다.. 😅

투명으로 지정해주면 되는것을..!!

왜 그랬는지 아래와같이 draw 메서드에 6,7번째 코드를 넣는것으로 해결을 했다.

 

CustomCell 코드

override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        guard completionRate > 0.0 else { return }
        
        UIColor.white.setFill()
        UIRectFill(rect)
            .
            .
            .
}

 

 

셀을 재사용할 때, 기존 셀의 상태를 정리하고 초기 상태로 되돌리는 것이 중요하다.

배경 색도 초기화의 일환으로 생각할 수 있다.

하지만 나는 배경색을 지정하지 않고 위에 코드로 처리를 한 것이다.

배경색이 기본값으로 설정되지 않을 경우 셀이 재사용될때 이전 상태를 유지할 수 있기 때문이다. 

처음 셀의 배경이 검정으로 나온건 검정색으로 설정된게 아닌 nil인 상태이다.

 

그래서 엄청 간단하게 아래 코드에서 14번째 코드로 해결을 했다.

 

 

CustomCell 코드

import UIKit
import FSCalendar

final class CustomCalendarCell: FSCalendarCell {

    var completionRate: Double? = nil {
        didSet {
            setNeedsDisplay()
        }
    }

    override init!(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .clear  // -> 배경 투명으로 해주기
    }

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

    override func prepareForReuse() {
        super.prepareForReuse()
        completionRate = nil
    }


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

        guard let completionRate = completionRate else { return }
        //titleLabel의 프레임 가져오기
        guard let titleLabel = self.titleLabel else { return }
        let labelFrame = titleLabel.frame

        let labelCenter = CGPoint(x: labelFrame.midX, y: labelFrame.midY)
        let pathRadius = min(bounds.width, bounds.height) / 2.0 - 8.0
        let circleRadius = min(bounds.width, bounds.height) / 2.0 - 6.0
        let startAngle = -CGFloat.pi / 2.0
        let endAngle = startAngle + CGFloat.pi * 2.0 * CGFloat(completionRate)

        //완료율 100% -> 빨간색 동그라미
        if completionRate == 1.0 {

            let opaqueCircle = UIBezierPath(arcCenter: labelCenter, radius: circleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
            UIColor.red.setFill()
            opaqueCircle.fill()

        //완료율 0% 초과 -> 각 퍼센테이지에 맞는 원형 선
        }else if completionRate > 0.0 {

            //배경(반투명)
            let backgroundPath = UIBezierPath(arcCenter: labelCenter, radius: pathRadius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
            UIColor.red.withAlphaComponent(0.2).setStroke()
            backgroundPath.lineWidth = 4.5 //두께
            backgroundPath.stroke()

            //완료율 그리기
            let foregroundPath = UIBezierPath(arcCenter: labelCenter, radius: pathRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
            UIColor.red.setStroke()
            foregroundPath.lineWidth = 4.5
            foregroundPath.stroke()

        //완료율 0% -> 반투명 빨간색 동그라미
        } else if completionRate == 0.0 {

            let translucentCircle = UIBezierPath(arcCenter: labelCenter, radius: circleRadius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
            UIColor.red.withAlphaComponent(0.2).setFill()
            translucentCircle.fill()
        }

    }
}

 

 

VC 의 cellFor at  메서드

  func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell {

        //캘린더의 날짜를 yyyy-mm-dd 형식으로 변경
        let todoDate = ymdDateFormat(date: date)
        let cell = calendar.dequeueReusableCell(withIdentifier: "CustomCalendarCell", for: date, at: position) as! CustomCalendarCell

        //데이터에서 해당날짜의 데이터가 있다면 -> 커스텀 셀 리턴
        if dicDataArray.keys.contains(todoDate) {
            var newCompletionRate: Double? = nil

            if let todos = dicDataArray[todoDate] {
                let completedCount = completedTodoCount[todoDate] ?? 0
                newCompletionRate = Double(completedCount) / Double(todos.count)
            }
            cell.completionRate = newCompletionRate
        }
        return cell
    }

 

 

 

 

 

해결 ) 2번 방법

FSCalendar 의 기본 Cell 을 등록해서 조건에 안맞을시  dequeueReusableCell 에 기본셀을 반환하는 방법 

 

 

FSCalendar  기본셀 등록 - 커스텀셀과 기본셀 모두 등록해준다.

final class CalendarTodoView: UIView {
	lazy var calendarView: FSCalendar = {
        let calendar = FSCalendar()
        calendar.register(CustomCalendarCell.self, forCellReuseIdentifier: "CustomCalendarCell")
        calendar.register(FSCalendarCell.self, forCellReuseIdentifier: "FSCalendarDefaultCell")
    	return calendar
    }
}

 

 

VC 의 cellFor at  메서드

func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell {
        
        //캘린더의 날짜를 yyyy-mm-dd 형식으로 변경
        let todoDate = ymdDateFormat(date: date)
        
        //데이터에서 해당날짜의 데이터가 있다면 -> 커스텀 셀 리턴
        if dicDataArray.keys.contains(todoDate) {
            let cell = calendar.dequeueReusableCell(withIdentifier: "CustomCalendarCell", for: date, at: position) as! CustomCalendarCell
            
            var newCompletionRate: Double? = nil
            
            if let todos = dicDataArray[todoDate] {
                let completedCount = completedTodoCount[todoDate] ?? 0
                newCompletionRate = Double(completedCount) / Double(todos.count)
            }
            cell.completionRate = newCompletionRate
            return cell
            
        //데이터에서 해당날짜의 데이터가 없다면 -> 기본셀 리턴
        } else {
            let defaultCell = calendar.dequeueReusableCell(withIdentifier: "FSCalendarDefaultCell", for: date, at: position) as! FSCalendarCell
            return defaultCell
        }
    }

 

이렇게 해주면 데이터가 있을때만 커스텀 셀을 사용하고 없을때는 기본 셀을 리턴하게 된다.

 

반응형