custom UIView.draw(_:) 와 UICollectionViewCell 재사용

EenSung Kim·2024년 4월 4일
0

iOS 앱개발 공부

목록 보기
10/10
contentMode = .redraw

해결책이 단순한 만큼 문제 해결 과정을 순서대로 적어보면 좋지 않을까 싶었습니다.

만약 draw(_:) 메서드를 오버라이드 해서 직접 뷰를 그려주고 있는 와중, 이 뷰가 재사용되는 상황이고 그래서 재사용되는 과정에서 뷰가 깨지거나 찌그러지는 현상을 겪고 계시다면? 이 방법이 해결책이 될 수도 있을 것 같아요!


문제

챗봇 앱을 개발하면서 말풍선을 커스텀하게 그려보았습니다. UIView 를 상속하는 커스텀 뷰를 만들고, draw(_:) 메서드를 오버라이드하여 내부에서 UIGraphicsGetCurrentContext() 를 활용해 그려주었는데요.

커스텀한 콜렉션뷰셀을 생성하여 여기에 직접 만든 뷰를 넣어주고 나니 문제가 발생했습니다. 콜렉션뷰를 위아래로 스크롤하게 되면 때때로 말풍선이 찌그러지는 경우가 발생한 것이죠.


해결해나갔던 과정

일단 print 를 찍어보자

override func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
        
    let padding: CGFloat = 10
    let startX: CGFloat = rect.minX + padding
    let startY: CGFloat = rect.minY + padding
    let endX: CGFloat = rect.maxX - padding
    let endY: CGFloat = rect.maxY - padding
    
    // 기타 다른 그리기 과정
}

코드를 보시면 draw(_:) 내부에서 파라미터로 제공된 rect 를 가져와서 기준점을 잡아주고 있었는데요. 재사용되는 과정에서 이 rect 에 변경이 생기는 것은 아닌지를 확인해 보았습니다.

확인해 봤더니 간혹 rect 의 정보가 약간 다르게 들어오는 경우들이 눈에 띄더라구요. 공식문서를 확인해보니, rect 의 경우 처음 호출될 때는 보통 전체 bounds 를 가리키지만 이후 과정에서는 뷰의 일부만을 반영하기도 한다는 내용이 있었습니다.

Debug View Hierarchy 확인

Debug View Hierarchy 로 디버깅도 해봤는데요. 채팅뷰의 범위는 충분히 잘 반영이 되고 있더라구요. (사실 내부에 textLabel 이 있었고, 이 textLabel의 텍스트는 잘 그려지는 것을 이미 확인하기도 했었어서..)

rect 가 변경된다면 bounds 를 쓰면 되지 않을까?

rect 가 바뀔 수도 있는 값이라 여기에 의존해서는 안된다면 bounds 를 쓰면 되지 않을까요?

...

라고 생각하고 rect 파라미터를 의존하고 있던 모든 코드들을 bounds 로 바꿔보았습니다. 그리고 나서 앱을 재실행했는데 결과는 여전히 동일하더라구요. 문제가 여전히 발생하는 것으로 보아 이것도 해결책은 아니었습니다.

prepareForReuse() ??

재사용에서 문제가 발생하는 경우 prepareForReuse() 를 오버라이드해서 문제를 해결하기도 하잖아요. 그래서 prepareForReuse() 를 오버라이드해보았습니다.

// 콜렉션뷰셀
override func prepareForReuse() {
    super.prepareForReuse()
    chatBubbleView.setNeedsDisplay()
}

setNeedsDisplay() 메서드 공식문서를 살펴보면 "to notify the system that your view’s contents need to be redrawn", 다시 말해 "뷰의 콘텐츠를 다시 그려야 한다는 걸 시스템에 알려주는 메서드" 라고 나와있는데요.

사실 이 방법도 해결책이 되지는 못했습니다. 문서를 조금 더 읽어보면 뷰가 즉시 다시 그려지는 것이 아니라, 다음 drawing cycle 에서 다시 그려진다고 나와있는데요. 아마도 문제가 발생하는 시점이 prepareForReuse() 에서 호출해서 반영되는 drawing cycle 보다 다음인 것이 아닐까 라고 추측을 해볼 수 있었습니다.


해결책

// draw 메서드를 오버라이드 해준 UIView
contentMode = .redraw

UIView 가 가진 contentMode 라는 프로퍼티를 지정해서 문제를 해결할 수 있었습니다. contentMode 공식문서를 읽어보면 이 프로퍼티는 viewbounds 가 바뀔 때 뷰의 레이어를 어떻게 조정할 것인지와 관련이 있다고 해요. 일일히 제가 언제 다시 그려야할지를 알려주기보다는 contentMode 속성을 지정함으로써 시스템이 알아서 판단하게끔 하는 것이죠.

contentMode 속성에는 UIView.ContentMode 라는 열거형을 할당하게 되어있습니다. 마찬가지로 공식문서에 보면 여러 유형을 확인할 수 있는데요. 이 중에 redraw 케이스를 활용했어요.

redrawviewbounds 가 변경되었을 때 뷰를 다시 그리는데요. 이때 setNeedsDisplay() 메서드를 호출하는 방식으로 동작한다고 합니다. 셀이 재사용되는 과정에서 draw(_:) 메서드가 언제 호출되는지는 알아내지 못했지만, contentModeredraw 로 지정함으로써 시스템에서 필요한 때에 적절하게 다시 뷰를 그려주도록 할 수 있었습니다.


구글링을 해봤을 때 말풍선을 직접 커스텀하게 그리는 방법은 많이 나와있는데, 저처럼 콜렉션뷰셀에 연동해서 활용하는 경우에 대해서는 정보를 찾아보기가 어려웠던 것 같아요. 아마도 굳이 이렇게 하는 경우가 없어서일 것 같습니다만.. 덕분에 뷰를 다시 그려야 하는 경우에 대해서 알아갈 수 있었던 것 같습니다.

이 글이 도움이 되었기를 바랄께요! 감사합니다~

profile
iOS 개발자로 전직하기 위해 공부 중입니다.

0개의 댓글

관련 채용 정보