iOS Study (Chapter 8 - 테이블 뷰와 데이터 소스 연동)
이 포스트는 꼼꼼한 재은씨의 스위프트 기본편을 보고 스스로 공부한 내용을 정리한 포스트 입니다.
예제 코드
참고 자료
테이블 뷰와 데이터 소스 연동
-
데이터 소스와 테이블 뷰를 연동하는 과정은 UITableViewDataSource라는 프로토콜에 의존하여 이루어진다.
-
테이블 뷰 컨트롤러는
이 프로토콜을 참고하여 지정된 메소드를 호출함으로써 데이터 소스와 테이블 뷰를 연동한다.
-
원래 대로라면 이 프로토콜을 상속받아야 하지만, 상속받고 있는 UITableViewController 클래스가 이미 해당 프로토콜을 상속받고 있으므로 다시금 상속받을 필요는 없다.
-
-
테이블 뷰에 데이터 소스를 연동할 때 필요한 내용은 다음 두 가지 이다.
-
1) 테이블이 몇 개의 행으로 구성되는가?
-
2) 각 행의 내용은 어떻게 구성되는가?
-
-
이들 두 가지 질문에 답하기 위한 메소드들이 UITableViewDataSource 프로토콜에 정의되어 있다.
이들 메소드를 구현하여, 실제로 앱이 구동될 때 메소드가 호출되고 그 결과로 적절한 반환값을 받아갈 수 있도록 해 주어야 한다.
데이터 소스 연동을 위한 핵심 메소드
테이블 뷰와 데이터 소스를 연동하는 데 필요한 기본 메소드는 다음과 같다.
// 첫 번째 기본 필요 메소드
tableView(_:numberOfRowsInSection:)
// 두 번째 기본 필요 메소드
tableView(_:cellForRowAt:)
-
이 메소드들은 iOS 시스템이 필요에 의해 호출하는 메소드들이다.
-
일종의
델리게이트 패턴
을 따르고 있다. -
동작이나 이벤트에 관한 메소드가 아니기 때문에 델리게이트라는 접미어를 붙이지는 않지만,
델리게이트 패턴과 동일한 방식으로 동작한다.
-
시스템이 호출하는 함수인 콜백 함수(Callback Function)로 생각하면 된다.
-
개발자가 알아서 적절한 시점에 호출하는 것이 아니라
작성해 두면 시스템이 알아서 호출하는 식이다.
-
tableView(_:numberOfRowsInSection:)
-
이 메소드는 테이블 뷰가 생성해야 할 행(Row)의 개수를 반환한다.
-
이 메소드는 iOS 시스템이 테이블 뷰를 구성하기 위해 먼저 호출하는 메서드이다.
-
이 메소드는
개발자가 사용하기 위한 것이 아니라시스템이 사용하기 위한 메소드이다.
-
다시 말해, 현재 몇 개의 행이 구성되어 있는지를 개발자에게 알려주는 역활이 아니라
몇 개의 행을 생성해야 할지 개발자가 iOS 시스템에게 알려주기 위해 작성하는 메소드이다.
-
더 정확히는 테이블 뷰를 구성하는 델리게이트에서 읽어 들이기 위한 용도인 것이다.
-
-
다시 한 번 강조하자면,
이 메소드는 이미 만들어진 테이블 뷰의 행 개수를 결과값으로 반환하는 용도가 아니다.이 메소드에 의해 테이블 뷰의 행 수가 결정되는 것이다.
-
대다수의
GUI 개발
에서 테이블 뷰의 행 수는 입력된 데이터 소스의 크기만큼 자동으로 만들어지지만,iOS에서 테이블 뷰를 구성할 때는 지정해 주는 개수만큼 행이 만들어진다.
-
이 메소드를 이용해서 테이블 뷰가 생성할 행의 개수를 작성해 놓으면 iOS 시스템은 메소드를 호출한 다음, 반환된 값만큼 목록을 생성한다.
-
따라서 준비된 데이터 소스의 배열(Array) 길이가 백만 개쯤 된다 하더라도 이 메소드가 반환하는 값이 10이라면 화면에서는 열 개의 목록밖에 표시되지 않는다.
-
반대로 데이터 소스에 저장된 아이템이 열 개밖에 되지 않는데 위 메소드에서 20을 반환해 버리면 iOS 시스템은 도합 스무 개의 행을 구성하기 위한 작업을 진행하게 된다.
-
대개는 실행하는 시점에서 오류가 발생한다.
-
그러니
생성해야 할 행 수는 개발자가 임의로 지정해주기보다는 데이터 소스의 크기를 동적으로 반환하는 방식으로 처리하는 것이 바람직하다.
-
-
// 이 메소드를 소스 코드에서 실제로 사용하는 형식.
// 인자의 종류와 타입이 함께 정의된 모습이다.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 테이블 뷰의 목록 길이
}
-
iOS가 이 메소드(tableView(_:numberOfRowsInSection:))를 호출할 때는 두 개의 인자값을 함께 전달한다.
-
1) 테이블 뷰 객체 정보
-
2) 섹션 정보
-
-
첫 번째 인자값(_ tableView: UITablevView, 테이블 뷰 객체 정보)은 이 메소드를 호출한 테이블 뷰 객체에 대한 정보를 나타낸다.
-
하나의 뷰 컨트롤러 내에 두 개 이상의 테이블 뷰가 존재할 수 있지만, iOS 프로토콜 기반 설계 방식의 특성으로 인하여 개별 테이블 뷰 각각에 대한 메소드를 구분해서 작성하기란 어렵다.
-
따라서 테이블 뷰가 여러 개일 때에도 모두 같은 메소드를 호출하게 된다.
- 이때,
호출되는 메소드 입장에서는 어느 테이블 뷰에서 자신을 호출하는지를 알 필요가 있기 때문에, 이를 위해 첫 번째 인자값이 사용된다.
- 이때,
-
-
두 번째 인자값(numberOfRowsInSection section: Int, 섹션 정보)은 섹션에 대한 정보이다.
-
테이블 뷰는 일종의 행 그룹의 개념인 섹션으로 이루어질 수 있고, 그 하위에 개별 행이 추가된다.
-
섹션별로 행의 수를 다르게 구성할 수 있기 때문에 섹션에 따라 구분하여 행의 개수를 반환해야 할 때도 있다.
-
필요에 따라서는 테이블 뷰 정보와 섹션 정보를 바탕으로 반환값을 다르게 줄 수도 있다.
- 예를 들어 이 테이블 뷰의 이 섹션은 3개의 행을 가져야 하고, 저 테이블 뷰의 저 섹션은 8개의 행을 가져야 한다라는 식.
-
tableView(_:cellForRowAt:)
-
이 메소드(tableView(_:cellForRowAt:))는 각 행이 화면에 표현해야 할 내용을 구성하는 데에 사용된다.
-
하지만
이 메소드가 반환하는 값은 전체 테이블 뷰의 목록이 아니라 하나하나의 개별적인 테이블 셀 객체
인데,이는 화면에 표현해야 할 목록의 수 만큼 이 메소드가 반복적으로 호출된다는 것을 의미
한다. -
메소드 내에서 테이블 뷰 셀 객체를 구성한 다음 결과값으로 반환하면 시스템은 이 객체를 받아 테이블 뷰의 목록 각 행에 채워 넣는 방식이다.
-
개발자가 작성한 데이터 소스는 이 메소드 내부에서 활용되어 특정 행의 콘텐츠를 구성하는 데에 사용된다.
-
iOS 시스템은 테이블 뷰를 구성하기 위해 먼저 'tableView(_:numberOfRowsInSection:)' 메소드를 호출하여 몇 개의 행을 생성해야 하는지를 반환받고, 그 수만큼 'tableView(_:cellForRowAt:)' 메소드를 호출한다.
매 호출 시마다 몇 번째 행에 대한 요청인지를 함께 전달하기 때문에 개발자는 이 값을 받아, 해당 행에 적절한 콘텐츠를 구성한 다음 이를 테이블 뷰 셀 객체 형태롤 리턴하면 된다.
-
이 메소드는 한 번 호출할 때마다 하나의 테이블 뷰 셀을 반환하므로 열 개의 행을 가진 목록을 구성하려면 모두 열 번의 호출이 필요하다(정확히는 아닐 수 있다).
// 인자값을 포함하여 소스 코드에서 사용되는 형식
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return 테이블 뷰 셀 인스턴스
}
-
iOS 시스템은 두 개의 인자값을 사용하여 이 메소드(tableView(_:cellForRowAt:))를 호출한다.
-
1) 구성할 테이블 뷰 객체에 대한 참조
-
2) 구성할 행에 대한 참조 정보
-
-
하나의 뷰 컨트롤러 안에 두 개 이상의 테이블 뷰가 사용될 경우, 첫 번째 인자값(_ tableView: UITableView, 구성할 테이블 뷰 객체에 대한 참조)으로 전달된 tableView 매개변수를 사용하면 어느 테이블 뷰에 대한 요청인지 쉽게 구분할 수 있다.
단순히 구분 용도만이 아니라 테이블 뷰 자체에 대한 참조가 필요할 때에도 사용할 수 있다.
-
첫 번째 매개변수를 통해 테이블 뷰가 특정되면, 이번에는
두 번째 매개변수인 'indexPath'를 통해 몇 번째 행을 구성하기 위한 호출인지 구분할 수 있다.
-
IndexPath 객체 타입으로 정의된 이 매개변수(cellForRowAt indexPath: IndexPath)는 선택된 행에 대한 관련 속성들을 모두 제공한다.
-
그중에서도
'.row'는 가장 많이 사용되는 속성으로, 행의 번호를 알려주는 역활을 한다.
0부터 시작하는 이 행 번호는 배열(Array)로 이루어진 데이터 소스의 아이템 인덱스와 대부분의 경우 일치하므로 이 속성을 사용하면 데이터 소스의 필요한 부분을 편리하게 읽어 들일 수 있다.
-
사용자 액션 처리를 위한 핵심 메소드 (tableView(_:didSelectRowAt:))
-
일반적으로 테이블 뷰를 구성할 때 많이 사용되는 핵심 메소드, tableView(_:didSelectRowAt:)
-
UITableViewDelegate 프로토콜에 정의된 이 메소드는 사용자가 목록 중에서 특정 행을 선택했을 때 호출된다.
-
보통 사용자가 선택한 내용에 맞는 액션을 처리하는 용도로 사용된다.
-
사용자가 행을 선택했을 때 딱히 처리해즐 액션 없이, 그저 화면에 목록을 표시하기만 하는 용도의 테이블 뷰라면 이 메소드를 구현할 필요는 없다.
-
반대로
사용자가 행을 선택했을 때 그에 맞게 화면을 이동하던가 혹은 상세 내용을 팝업으로 보여주는 등의 다양한 기능을 구현하고 싶다면 이 메소드를 구현해 주어야 한다.
개발자는 이 메소드 내부에 원하는 로직을 작성하면 된다.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
-
이 메소드(tableView(_:didSelectRowAt:))는 델리게이트 메소드이기 때문에 적절한 시점에 맞추어 자동으로 호출된다.
-
'tableView(_:numberOfRowsInSection:)'
과'tableView(_:cellForRowAt:)'는
테이블 뷰를 화면에 구현할 때 호출
되는데 반해'tableView(_:didSelectRowAt:)' 메소드는 사용자의 액션이 있을 때 호출
된다는차이점
이 있다. -
iOS 시스템은 두 개의 인자값을 함께 전달한다.
-
1) 사용자가 터치한 테이블 뷰에 대한 참조값
-
2) 터치된 행에 대한 정보
-
-
이 두가지 인자값을 이용하여 개발자는 사용자가 어느 테이블 뷰의 몇 번째 행을 선택했는지를 확인 할 수 있으며, 새로운 화면으로 이동하는 코드를 작성하거나 알림창, 혹은 기타 기능을 작성하는 과정을 구현할 수 있다.
메소드 구현 실습
-
테이블 뷰 행의 개수를 반환하는 메소드를 작성한다.
-
이 메소드는
생성해야 할 행의 개수를 반환하는 메소드이다.
iOS 시스템은 이 메소드가 반환하는 값만큼의 테이블 뷰 행을 생성한다.
-
메소드 호출 시 함께 전달되는 두 개의 매개변수 'tableView'와 'section'은 각각 어느 테이블 뷰인지, 그리고 테이블 뷰 내에서도 몇 번째 섹션에 대한 호출인지를 알려준다.
-
만약 여러 개의 테이블 뷰나 섹션이 존재한다면 두 개의 매개변수(tableView, section)를 통하여 어느 테이블 뷰의 어느 섹션인지를 구분하고, 이에 맞는 값을 반환하는 과정을 메소드 내에 추가해야 한다.
-
특별한 이유가 있지 않는 한, 테이블 뷰를 구성하는 행의 개수는 데이터 소스의 크기와 일치해야한다.
-
따라서
이 메소드가 반환하는 값도 데이터 소스의 크기와 일치해야 한다. 고정값으로 반환값을 지정해 줄 수도 있지만(이를 '하드 코딩'이라고 부른다), 데이터 소스의 크기가 변경될 때마다 수정해 주어야 하므로 그보다는 데이터 소스의 크기가 바뀔 때마다 반환값도 함께 바뀌도록 처리해 주는 것이 좋다.
-
'.count' 속성은 배열(Array) 타입 객체의 길이를 가져오는 값으로, 데이터 소스 전체의 크기를 알기 위해 사용되었다.
-
-
테이블 뷰 행을 구성하는 메소드이다.
-
이 메소드는
개별 행을 만들어내는 역활을 한다.
-
'tableView(_:numberOfRowsInSection:)' 메소드가 반환하는 값만큼 이 메소드(tableView(_:cellForRowAt:))가 반복 호출된다.
-
이 메소드(tableView(_:cellForRowAt:))가 한 번 호출될 때마다 하나의 행이 만들어진다고 생각하면 된다.
-
몇 번째 행을 구성해야 하는지 알려주기 위해 'IndexPath' 타입의 객체가 인자값으로 전달된다.
-
행 번호를 알고자 할 때에는 'indexPath.row' 속성을 사용하면 된다.
-
이 속성은 배열과 마찬가지로 0부터 시작한다.
-
첫 번째 행이면 0을, 두 번째 행이면 1을 반환하는 식이다.
-
이렇게 0부터 시작되는 행 번호는 배열의 인덱스와 일치
하기 때문에 +1 또는 -1 할 필요 없이배열 형식의 데이터 소스의 인덱스로 바로 사용할 수 있다.
- 위 메소드에서 가장 먼저 처리하고 있는 것은
이 속성을 사용하여 'self.list' 배열로부터 데이터 소스를 읽어오는 것이다.
- 위 메소드에서 가장 먼저 처리하고 있는 것은
-
Reusable Queue
-
데이터가 준비되고 나면 이제 해야 할 일은 테이블 뷰 셀 객체를 만들어내는 일이다.
-
테이블 뷰 셀 객체는 담당 클래스인 UITableViewCell을 초기화하여 생성할 수도 있다.
-
테이블 뷰 셀 객체를 직접 생성하는 대신 미리 정해진 특정 메소드를 이용하여 간접적으로 만들어낼 수도 있다.
-
이때 사용되는 메소드가 'dequeueReusableCell(withIdentifier:)'이다.
-
이 메소드는 인자값으로 입력받은 아이디를 이용하여 스토리보드에 정의된 프로토타입 셀을 찾고, 이를 인스턴스로 생성하여 개발자에게 제공한다.
-
스토리보드의 프로토타입 셀에 설정해주었던 Identifier 속성이 프로토타입 셀을 식별하기 위해 사용된다.
-
-
-
테이블 뷰 객체가 제공하는 재사용 큐는 한 차례 사용된 테이블 셀 인스턴스가 폐기되지 않고 재사용을 위해 대기하는 공간으로, 만약 dequeueReusableCell(withIdentifier:) 메소드가 호출되었을 때 입력된 아이디에 맞는 인스턴스가 큐에 있다면 이 인스턴스를 꺼내어 재사용하고, 만약 입력된 아이디에 맞는 인스턴스가 큐에 없다면 새로 생성하여 제공하는 방식으로 동작한다.
-
입력된 인자값에 대한 프로토타입 셀이 존재하지 않을 경우를 상정하여, dequeueReusableCell(withIdentifier:) 메소드의 결과값은 옵셔널 타입으로 반환된다.
-
하지만 개발자가 확실하게 아이디를 입력해주었다면 실제로 nil 값이 반환될 가능성은 없다.
-
재사용 큐를 사용하여 셀 객체를 만들어 내는 과정은 정부가 은행을 통해 돈을 발행하는 과정과 비슷하다.
-
정부가 직접 돈을 찍어낼 수도 있지만, 이렇게 될 경우 마음대로 찍어내어 급격한 인플레이션을 초래할 수도 있고 통화 조절 기능을 제대로 수행하기 어려우므로 국책 은행에 화폐 발행권을 위임하고, 화폐 발행이 필요할 경우 해당 은행에 요청하는 식이다.
-
은행 내부적으로 보유하고 있는 화폐가 있을 경우 이를 풀고, 없을 경우에는 화폐를 추가 발행하여 유통시킨다.
-
- 위의 은행 예시와 같이
개발자도 직접 UITableViewCell() 구문을 통해 셀 객체를 생성하는 대신 dequeueReusableCell(withIdentifier:)를 통해 셀 객체를 요청하고, 그 결과로 얻은 셀 객체를 반환값으로 사용하여 화면에 풀어놓는다.
셀의 기본 속성을 사용하여 행의 내용 구성
-
셀 객체가 반환되면 이제 테이블 뷰 셀의 기본 속성을 사용하여 행의 내용을 구성할 차례이다.
-
UITableViewCell 객체의 속성 중 textLable이 있다.
셀에서 제목을 표시하는데 사용되는 속성이다.
-
textLabel은 UILabel 타입으로 정의된 속성이다.
- 레이블에 텍스트를 표현했던것 처럼
이 속성의 하위 속성인 .text를 통해 원하는 문자열을 레이블로 표현할 수 있다.
- 레이블에 텍스트를 표현했던것 처럼
-
영화 데이터 중에서 제목에 해당하는 값이 'row.title' 이므로, 이 값을 해당 속성에 대입하면 테이블 목록에는 영화의 제목이 표현된다.
옵셔널 체인 (Optional Chaining)
-
cell.textLabel 구문에서 한 가지 더 살펴봐야 할 것은
옵셔널 체인
이다. -
옵셔널 체인은 옵셔널로 선언된 객체를 사용할 때 매번 nil 여부를 체크해야 하는 비효율성을 줄이기 위한 문법으로, 옵셔널 타입의 객체와 그의 속성 사이에서 물음표(?) 연산자를 통해 구현된다.
이렇게 작성된 옵셔널 타입은 값이 있을 경우 작성된 내용을 정상적으로 실행하지만, 값이 비어 있더라도 실행을 건너뛸 뿐 오류를 발생시키지 않는다.
-
프로토타입 셀
에 Basic, Right Detail, Left Detail, Subtitle스타일이 있다.
-
이들 타입은 모두 제목을 가진다. 다시 말해 textLabel 속성에 값이 저장되어 있는 것이다.
- 따라서
해당 속성을 사용하여 제목을 간단하게 화면에 표시할 수 있다.
- 따라서
-
-
Custom 타입에는 textLabel 속성이 정의되어 있지 않다.
-
개발자가 원하는 대로 셀을 구현하기 위해 다른 어떤 기본 텍스트 속성도 지원하지 않는 것이다.
-
따라서
주어진 셀의 타입이 만약 Custom으로 설정되어 있다면 textLabel 속성에는 값이 비어있는 상태가 된다.
-
이처럼
값이 비어있을 가능성이 있는 변수는 오류 방지와 간결한 처리를 위해 옵셔널 타입으로 처리하는 것이 스위프트의 특징이다.
- 그래서
테이블 뷰 셀의 하위 속성인 textLabel은 옵셔널 타입으로 정의
된다.
- 그래서
-
-
만약 옵셔널 체인을 사용하지 않는다면 위 코드는 textLabel 속성이 있는지를 검사하기 위한 조건절이 포함되어야 하고, 이는 코드를 복잡하게 만드는 요인이 된다.
-
반대로 말하면
옵셔널 체인 덕분에 코드를 한 줄로 간결하게 작성할 수 있다.
tableView(_:didSelectRowAt:)
-
테이블 셀을 클릭하거나 터치했을 때 액션을 처리해주려면 이 메소드를 구현해야 한다.
-
사용자가 셀을 선택하면 델리게이트 시스템에 의해 이 메소드가 호출되기 때문이다.
-
상세 내용을 보여주기 위해 화면을 전환한다든가 하는 작업 등이 이 메소드 내에서 구현될 수 있다.
-
메소드 정리
-
1) tableView(_numberOfRowsInSection:) - 메소드를 구현하고, 생성할 목록의 길이를 반환한다.
-
2) tableView(_:cellForRowAt:) - 메소드를 구현하고, 셀 객체를 생성하여 콘텐츠를 구성한 다음 반환한다.
-
3) tableView(_:didSelectRowAt:) - 메소드를 구현하고, 사용자가 셀을 선택했을 때 실행할 액션을 정의한다.