[Swift - UIKit] FSCalendar CustomCell 재사용 문제 - 완전해결
저번 셀의 재사용 문제 포스팅을 했는데 완벽한 해결은 아니였어서 다른방법을 찾던중 해결을 하여 포스팅 합니다.
문제
어떤 문제였는지는 저번포스팅을 참고해주세요~
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
}
}
이렇게 해주면 데이터가 있을때만 커스텀 셀을 사용하고 없을때는 기본 셀을 리턴하게 된다.