이 포스트는 꼼꼼한 재은씨의 스위프트 기본편을 보고 스스로 공부한 내용을 정리한 포스트 입니다.


예제 코드


참고 자료


테이블 뷰와 데이터 소스 연동

  • 데이터 소스와 테이블 뷰를 연동하는 과정은 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) 터치된 행에 대한 정보

  • 이 두가지 인자값을 이용하여 개발자는 사용자가 어느 테이블 뷰의 몇 번째 행을 선택했는지를 확인 할 수 있으며, 새로운 화면으로 이동하는 코드를 작성하거나 알림창, 혹은 기타 기능을 작성하는 과정을 구현할 수 있다.


메소드 구현 실습

methodExampleImage-1

  • 테이블 뷰 행의 개수를 반환하는 메소드를 작성한다.

  • 이 메소드는 생성해야 할 행의 개수를 반환하는 메소드이다.

    • iOS 시스템은 이 메소드가 반환하는 값만큼의 테이블 뷰 행을 생성한다.
  • 메소드 호출 시 함께 전달되는 두 개의 매개변수 'tableView'와 'section'은 각각 어느 테이블 뷰인지, 그리고 테이블 뷰 내에서도 몇 번째 섹션에 대한 호출인지를 알려준다.

  • 만약 여러 개의 테이블 뷰나 섹션이 존재한다면 두 개의 매개변수(tableView, section)를 통하여 어느 테이블 뷰의 어느 섹션인지를 구분하고, 이에 맞는 값을 반환하는 과정을 메소드 내에 추가해야 한다.

  • 특별한 이유가 있지 않는 한, 테이블 뷰를 구성하는 행의 개수는 데이터 소스의 크기와 일치해야한다.

    • 따라서 이 메소드가 반환하는 값도 데이터 소스의 크기와 일치해야 한다. 고정값으로 반환값을 지정해 줄 수도 있지만(이를 '하드 코딩'이라고 부른다), 데이터 소스의 크기가 변경될 때마다 수정해 주어야 하므로 그보다는 데이터 소스의 크기가 바뀔 때마다 반환값도 함께 바뀌도록 처리해 주는 것이 좋다.

    • '.count' 속성은 배열(Array) 타입 객체의 길이를 가져오는 값으로, 데이터 소스 전체의 크기를 알기 위해 사용되었다.

methodExampleImage-2

  • 테이블 뷰 행을 구성하는 메소드이다.

  • 이 메소드는 개별 행을 만들어내는 역활을 한다.

  • 'tableView(_:numberOfRowsInSection:)' 메소드가 반환하는 값만큼 이 메소드(tableView(_:cellForRowAt:))가 반복 호출된다.

  • 이 메소드(tableView(_:cellForRowAt:))가 한 번 호출될 때마다 하나의 행이 만들어진다고 생각하면 된다.

  • 몇 번째 행을 구성해야 하는지 알려주기 위해 'IndexPath' 타입의 객체가 인자값으로 전달된다.

  • 행 번호를 알고자 할 때에는 'indexPath.row' 속성을 사용하면 된다.

    • 이 속성은 배열과 마찬가지로 0부터 시작한다.

    • 첫 번째 행이면 0을, 두 번째 행이면 1을 반환하는 식이다.

    • 이렇게 0부터 시작되는 행 번호는 배열의 인덱스와 일치하기 때문에 +1 또는 -1 할 필요 없이 배열 형식의 데이터 소스의 인덱스로 바로 사용할 수 있다.

      • 위 메소드에서 가장 먼저 처리하고 있는 것은 이 속성을 사용하여 'self.list' 배열로부터 데이터 소스를 읽어오는 것이다.

Reusable Queue

  • 데이터가 준비되고 나면 이제 해야 할 일은 테이블 뷰 셀 객체를 만들어내는 일이다.

  • 테이블 뷰 셀 객체는 담당 클래스인 UITableViewCell을 초기화하여 생성할 수도 있다.

  • 테이블 뷰 셀 객체를 직접 생성하는 대신 미리 정해진 특정 메소드를 이용하여 간접적으로 만들어낼 수도 있다.

    • 이때 사용되는 메소드가 'dequeueReusableCell(withIdentifier:)'이다.

      • 이 메소드는 인자값으로 입력받은 아이디를 이용하여 스토리보드에 정의된 프로토타입 셀을 찾고, 이를 인스턴스로 생성하여 개발자에게 제공한다.

      • 스토리보드의 프로토타입 셀에 설정해주었던 Identifier 속성이 프로토타입 셀을 식별하기 위해 사용된다.

ReusableQueueImage-1

  • 테이블 뷰 객체가 제공하는 재사용 큐는 한 차례 사용된 테이블 셀 인스턴스가 폐기되지 않고 재사용을 위해 대기하는 공간으로, 만약 dequeueReusableCell(withIdentifier:) 메소드가 호출되었을 때 입력된 아이디에 맞는 인스턴스가 큐에 있다면 이 인스턴스를 꺼내어 재사용하고, 만약 입력된 아이디에 맞는 인스턴스가 큐에 없다면 새로 생성하여 제공하는 방식으로 동작한다.

  • 입력된 인자값에 대한 프로토타입 셀이 존재하지 않을 경우를 상정하여, dequeueReusableCell(withIdentifier:) 메소드의 결과값은 옵셔널 타입으로 반환된다.

  • 하지만 개발자가 확실하게 아이디를 입력해주었다면 실제로 nil 값이 반환될 가능성은 없다.

  • 재사용 큐를 사용하여 셀 객체를 만들어 내는 과정은 정부가 은행을 통해 돈을 발행하는 과정과 비슷하다.

    • 정부가 직접 돈을 찍어낼 수도 있지만, 이렇게 될 경우 마음대로 찍어내어 급격한 인플레이션을 초래할 수도 있고 통화 조절 기능을 제대로 수행하기 어려우므로 국책 은행에 화폐 발행권을 위임하고, 화폐 발행이 필요할 경우 해당 은행에 요청하는 식이다.

    • 은행 내부적으로 보유하고 있는 화폐가 있을 경우 이를 풀고, 없을 경우에는 화폐를 추가 발행하여 유통시킨다.

ReusableQueueImage-2

  • 위의 은행 예시와 같이 개발자도 직접 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:)

methodExampleImage-3

  • 테이블 셀을 클릭하거나 터치했을 때 액션을 처리해주려면 이 메소드를 구현해야 한다.

    • 사용자가 셀을 선택하면 델리게이트 시스템에 의해 이 메소드가 호출되기 때문이다.

    • 상세 내용을 보여주기 위해 화면을 전환한다든가 하는 작업 등이 이 메소드 내에서 구현될 수 있다.


메소드 정리

  • 1) tableView(_numberOfRowsInSection:) - 메소드를 구현하고, 생성할 목록의 길이를 반환한다.

  • 2) tableView(_:cellForRowAt:) - 메소드를 구현하고, 셀 객체를 생성하여 콘텐츠를 구성한 다음 반환한다.

  • 3) tableView(_:didSelectRowAt:) - 메소드를 구현하고, 사용자가 셀을 선택했을 때 실행할 액션을 정의한다.