TableView를 활용한 투 두 앱을 만들던 도중에
할 일 데이터를 추가하다보니
뜻하지 않게 UI가 변경되는 것을 확인하였습니다.
그래서 어떤 걸 겪었고, 그렇게 된 이유는 무엇이고, 어떻게 개선했는지 기록하고자 합니다 !!
현재 발생한 문제점
위의 GIF와 같이 할 일 두 개를 추가하고 나면
스위치의 on / off 상태가 바뀌는것을 확인하였습니다.
Label이 바뀌는것은 무시해주세요 !
그래서 저는 추가 버튼을 누르는 것 부터,
UI가 변경되기까지의 과정을 하나하나 검증해보았습니다 !
무엇이 원인인가?
추가 버튼을 클릭 하였을 때 내부 흐름은 다음과 같습니다.
1) 투 두 관리 객체에 투 두 추가
2) reloadData() 호출
1) 까지만 실행을 하면
단순히 내부적으로 데이터만 추가하기 때문에
UI에는 어떠한 영향도 끼치지 않습니다.
따라서 2)에서 발생한 문제라고 할 수 있겠습니다.
reloadData() 란 무엇인가?
"테이블 뷰의 행과 세션들을 다시 로드합니다."
또한 reloadData()는 다음과 같은 특징들을 가지고 있습니다.
(1)
cell, section header와 footer, 인덱스 배열 등
테이블을 구성하는데 사용되는 모든 데이터를 reload할 때 호출합니다.
(2)
효율성을 위해 테이블 뷰는 보이는 행만 다시 표시합니다.
(3)
reload의 결과로 인해 테이블이 작아지면 offset을 조정합니다.
(4)
테이블 뷰의 delegate와 data source는
테이블 뷰가 데이터를 완전히 reload 하려는 경우,
이 메소드를 호출합니다.
(5)
행을 삽입하거나 삭제하는 메소드,
특히 beginUpdates() 및 endUpdates() 호출로 구현 된
애니메이션 블록 내에서 호출해서는 안 됩니다.
(6)
hasUncommittedUpdates 속성이 true인 경우
이 메소드를 호출하면 안됩니다.
이렇게 하면 테이블 뷰가 데이터를 다시 로드하기 전에
커밋되지 않은 변경 사항을 강제로 삭제합니다.
..
그렇다고 합니다...ㅎㅎ
행을 삽입하거나 삭제하는 메소드에서는
reloadData()를 호출해서는 안 된다고 했는데
호출해서 생긴 문제였습니다.
+)
이 프로젝트에서는
Cell이 처음 만들어지거나 재배치 될 때
cellForRowAt 내부에서 Switch의 상태 값이 저장 된 속성의 값을
Switch에 할당 해주지 않았고,
이후 Cell이 재사용 될 때
prepareForReuse() 메소드를 통해
Cell의 내부 속성들을 초기화하지 않았기 때문에,
밑에서 작성 할 dequeueReusableCell와 연쇄되어
발생 된 문제이기도 합니다.
하지만 저는 reloadData()가 호출 된 후 어떠한 과정을 거쳐서
GIF의 최종 결과물까지 도출 되었는지 궁금해졌습니다.
reloadData()가 호출 된 후, UI의 변경까지 과정
reloadData()가 호출되면 테이블을 구성하는데 사용되는 모든 데이터를 reload합니다.
따라서 reload된 새로운 데이터들을 배치하기 위해
1) numberOfRowsInSection
2) cellForRowAt
위의 두 개의 메소드가 호출됩니다.
1)은 개수와 관련 된 메소드이니 2)를 통해 UI가 바뀌었다고 할 수 있습니다.
결론부터 말씀드리자면
(1)
위에서 적은 거 처럼 Cell을 재사용할 때
Cell 내부 속성들의 값을 초기화 하지 않았기 때문에
각 Cell마다 Switch의 On / Off 가
Cell에 그대로 남아있게 됩니다.
(2)
새로운 투 두를 제외한 나머지는
dequeueReusableCell로 인해 Cell이 재사용 되기 때문에,
각각의 Cell들이 Switch의 On / Off 가 유지된 채로
dequeueReusableCell의 내부 로직으로 인해 순서가 바뀌게 되고,
이후에 데이터가 할당되어 배치되게 됩니다.
(3)
따라서 reloadData() 메소드가 끝난 이후
재배치된 Cell들의 Label은 재배치 여부와 상관없이
데이터의 값이 순서대로 할당되지만,
Switch의 On / Off는 할당하지 않았기 때문에
기존 Cell에 남아있던 On / Off 가
그대로 반영되게 됩니다 !
밑의 표는 순서에 따라 Cell이 재배치된 모습입니다.
인덱스 | 할 일 추가하기 전 | aaa를 추가한 후 | bbb를 추가한 후 |
0 | 0001(Cell 주소) / 스위치 On | 0005(Cell 주소) / 스위치 On | 0006(Cell 주소) / 스위치 Off |
1 | 0002(Cell 주소) / 스위치 Off | 0004(Cell 주소) / 스위치 Off | 0001(Cell 주소) / 스위치 On |
2 | 0003(Cell 주소) / 스위치 On | 0003(Cell 주소) / 스위치 On | 0002(Cell 주소) / 스위치 Off |
3 | 0004(Cell 주소) / 스위치 Off | 0002(Cell 주소) / 스위치 Off | 0003(Cell 주소) / 스위치 On |
4 | 0005(Cell 주소) / 스위치 On | 0001(Cell 주소) / 스위치 On | 0004(Cell 주소) / 스위치 Off |
5 | 0006(Cell 주소) / 스위치 Off | 0005(Cell 주소) / 스위치 On | |
6 | 0007(Cell 주소) / 스위치 Off |
1)
aaa를 추가한 후에는 Cell의 재배치가 일어났지만
스위치 On / Off가 할 일 추가하기 전과 동일하게 변경되었기 때문에
GIF에서는 변경되지 않은 거처럼 보이게 됩니다.
2)
하지만 bbb를 추가한 후에는 Cell이 표와 같이 재배치되었기 때문에
GIF에서는 변경되어서 보이게 된 것입니다.
어떻게 개선했는가?
위에서 언급한 거 처럼
Cell이 처음 만들어지거나 재배치 될 때
cellForRowAt 내부에서 Switch의 상태 값이 저장 된 속성의 값을
Switch에 할당 해주게 된다면
reloadData() 메소드를 사용해도 문제가 없을 수도 있습니다.
하지만 공식문서에서도 행을 삽입 할 때
reloadData() 메소드를 사용하면 안된다고 했었고,
무엇보다 reloadData() 메소드는 모든 데이터를 reload하기 때문에
효율적이지 않습니다.
따라서 행을 삽입해야 한다면
insertRows(at:with:)를 사용하는게 올바르다고 할 수 있습니다.
밑의 표는 insertRows(at:with:)를 사용할 때 순서에 따라 Cell이 재배치된 모습입니다.
인덱스 | 할 일 추가하기 전 | aaa를 추가한 후 | bbb를 추가한 후 |
0 | 0001(Cell 주소) / 스위치 On | 0001(Cell 주소) / 스위치 On | 0001(Cell 주소) / 스위치 On |
1 | 0002(Cell 주소) / 스위치 Off | 0002(Cell 주소) / 스위치 Off | 0002(Cell 주소) / 스위치 Off |
2 | 0003(Cell 주소) / 스위치 On | 0003(Cell 주소) / 스위치 On | 0003(Cell 주소) / 스위치 On |
3 | 0004(Cell 주소) / 스위치 Off | 0004(Cell 주소) / 스위치 Off | 0004(Cell 주소) / 스위치 Off |
4 | 0005(Cell 주소) / 스위치 On | 0005(Cell 주소) / 스위치 On | 0005(Cell 주소) / 스위치 On |
5 | 0006(Cell 주소) / 스위치 Off | 0006(Cell 주소) / 스위치 Off | |
6 | 0007(Cell 주소) / 스위치 Off |
insertRows(at:with:)를 사용한다면 기존의 Cell은 유지한 채
추가되는 Cell만 새로 생성해서 데이터를 할당하기 때문에
효율적이라고 할 수 있습니다.
결론적으로,
(1)
Cell이 처음 만들어지거나 재배치 될 때
cellForRowAt 내부에서
Switch의 상태 값이 저장 된 속성의 값을 Switch에 할당하였고
(2)
reloadData()를 사용함으로써 생기는 문제들을
insertRows(at:with:)를 통해 개선할 수 있었습니다.
이번 기회를 통해서 많이 부족하지만
reloadData() 메소드에 대해 뼈대만큼은
꼼꼼하게 정리하려고 했던 거 같습니다.
앞으로도 공부하고 정리해야 할 내용이 많지만,
이러한 습관을 지켜나가겠습니다.
읽어주셔서 감사합니다 :)