[Troubleshooting] tableViewCell에서 제약조건이 동작하지 않을 때
이번 포스트는 tableViewCell에서 제약조건이 동작하지 않아 고생했던 경험을 나누려고 합니다.
스토리보드에서 tableViewCell안에 여러개의 view를 추가한 다음에 제약조건을 작성했습니다. 그다음 앱을 실행해서 확인해 보았는데 제약조건이 전혀 동작하지 않았습니다.
원인은 tableViewCell의 contentView에 CustomView를 설정해서 제약조건이 동작하지 않았습니다. 왜 contentView를 사용자화하면 문제가 발생하는지 궁금했습니다.
원인을 파악하기 위해서 그날의 상황을 가정하여 간단한 프로젝트를 만들어 재현해보겠습니다.
먼저, 아래 그림처럼 스토리보드를 구성했습니다. 메인화면에서 move 버튼을 선택하면 tableView가 보이도록 단순하게 되어 있습니다.
TableViewController는 TestTableViewController를 만들어 설정했고 cell은 TestTableViewCell로 만들었습니다. contentView는 TestView로 설정하고 TestTableViewCell에 titlte property를 만들어서 스토리보드에 연결시켰습니다.
제약 조건은 아래처럼 설정했습니다.
TestTableViewController는 아래 코드처럼 3개의 항목이 보이도록 작성했습니다.
class TestTableViewController: UITableViewController {
let datas = ["1. Test", "2. Test", "3. Test"]
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datas.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath)
(cell as? TestTableViewCell)?.title.text = datas[indexPath.row]
return cell
}
실행결과는 아래와 같습니다.
제약 조건이 잘 동작할거라 생각했지만 전혀 동작하지 않습니다.
원인을 파악하기 위해 디버깅 모드로 cell이 생성되는 시점을 확인해 보겠습니다.
(lldb)po cell.contentView
cell이 생성된 후 디버그창에서 위와 같이 contentView를 확인해 보면 스토리보드에서 설정한 TestView가 아니라 UITableViewCellContentView가 되어 있는 것을 확인할 수 있습니다.
스토리보드에서 생성한 제약조건은 TestView에 추가한 내용인데 contentView가 TestView가 아니라 UITableViewCellContentView로 변경되어 있기 때문에 제약조건이 동작하지 않았던 것이었습니다.
디버깅을 통해 contentView에 CustomView를 설정할 수 없다는 것을 확인했습니다.
그렇다면 스토리보드를 통해 TestView는 생성되지 않는 것일까요?
한번 확인해 보겠습니다.
TestView 클래스 안에 아래처럼 코드를 작성했습니다.
class TestView: UIView {
required init?(coder: NSCoder) {
super.init(coder: coder)
print("init")
}
deinit {
print("deinit")
}
}
두 곳에 break point를 걸어서 확인해보겠습니다.
TestView의 requited init?(coder: NSCoder)가 호출됩니다.
call stack에서 호출한 메소드를 보면 UITableViewAccessibility의 dequeueReusableCellWithIdentifier:forIndexPath:임을 알 수 있습니다.
디버거 보드를 계속 진행하면 init이 호출된 후 바로 deinit이 호출됩니다.
call stack에서 deinit을 호출한 메소드를 찾아보면 init을 호출했을 때와 동일한 위치에서 호출됩니다.
이것을 통해 TestView가 스토리보드를 통해서 생성되지만 그 이후 바로 해제되는 것을 확인할 수 있습니다. contentView는 UITableViewCellContentView만 될 수 있으며 UITableViewCellContentView가 아니면 안되는 것을 확인했습니다.
그렇다면 TestView를 UITableViewCellContentView의 자식클래스로 만들면 설정이 가능할까요?
안타깝게도 UITableViewCellContentView는 공개클래스가 아니어서 직접 상속받아 구현할 수가 없습니다.
TestView를 UITableViewCellContentView의 자식으로 속이기
TestView를 생성한 후 바로 해제하는 걸로 미루어보아 contentView를 생성할 때 class 종류를 확인할 것이란 생각이 들었습니다.
TestView 클래스에 아래 코드를 추가해서 class확인 작업을 우회해 보겠습니다.
override func isKind(of aClass: AnyClass) -> Bool {
if aClass == NSClassFromString("UITableViewCellContentView") {
return true
}
return super.isKind(of: aClass)
}
이제 앱을 실행하고 cell이 생성되는 시점에 확인해 보겠습니다.
cell의 contentView가 TestView로 생성되는 것을 확인할 수 있습니다. 이것으로 contentView 생성시에 class의 type을 확인한 후 UITableViewCellContentView일 때만 사용한다는 것을 확실히 알 수 있습니다.
디버깅모드에서 계속 진행하면 아래같이 runtime error가 발생합니다.
TestView는 UITableViewCellContentView의 자식이 아닌 UIView의 자식이기 때문에 오류가 발생하는 것은 당연합니다. '_setOverriddenDefaultLayoutMargins’라는 내부 메소드가 없어서 오류가 발생했는데 이 부분도 UITableViewCellContentView처럼 보이도록 하기 위해서 TestView에 아래 코드를 추가합니다.
@objc func _setOverriddenDefaultLayoutMargins(_ t: Any) {
// nothing
}
다시 앱을 실행하면 이젠 오류 없이 진행되며 아래와 같이 제한사항이 적용된 정상적인 화면이 보입니다.
이 과정을 통해 tableViewCell의 contentView는 UITableViewCellContentView거나 또는 그 자식이어야한다는 것을 확인했습니다. UITableViewCellContentView처럼 보이도록 속여서 제한조건이 동작하도록 처리했지만 위의 방식처럼 사용하지 말아야합니다. UITableViewCellContentView는 비공개된 클래스로 서브클래싱할 수 없기 때문입니다.
스토리보드를 사용할 때, 스토리보드에서 설정할 수 있다고 해서 아무 view나 임의로 사용자화한다면 예상하지 못한 문제를 접할 수 있으므로 각 view의 명확한 사용법 또는 구조를 이해하고 사용하는 것이 필요합니다.
댓글
댓글 쓰기