아이폰 앱 리스트 만들기
컬렉션 뷰(Collection View)
지난 포스팅에서는 테이블 뷰(Table View)를 이용한 리스트를 만들었었다.
하.지.만
테이블 뷰(Table View)가 대표적인 리스트를 만드는 방식이기는 하나 조금 더 간지나는, 더 이쁜 리스트를 만들고 싶을 때도 있다. 사진첩과 같은 그리드 형식의 리스트가 좋은 예다.
마침 애플은 그런 모양의 리스트를 만들 수 있도록 컬렉션 뷰(Collection View)를 제공하고 있다. 그리고 방법은 테이블 뷰(Table view)를 구현하는 것과 크게 다를 것이 없다. (새로 배울 게 별로 없다니 이 얼마나 다행인가!)
*그리드(Grid) : 격자 형식의 무늬
스토리보드(Storyboard) 만들기
앱을 만들고자 Xcode 를 열면, 스토리보드(Storyboard)에 하나의 기본 뷰 컨트롤러(View Controller)가 있다. 이 뷰 컨트롤러에 아래와 같이 컬렉션 뷰(Collection View)를 끌어와 배치한다.
범위는 화면 전체로 넓혀서 사용할 수 있다.


이제 컬렉션 뷰(Collection View)를 사용하기 위해서 DataSource 및 Delegate 프로토콜을 연결한다. 컬렉션 뷰(Collection View)를 선택해서 왼쪽의 인스펙터를 보면 연결 상태를 확인할 수 있다.

그 다음으로 이미지 등 아이템이 보일 셀(Cell)에 ID를 부여해준다. 화면에서 희미하게 보이는 네모칸이 바로 아이템 셀(Item Cell)이다. 셀(Cell)을 선택 후, 인스펙터에서 Identifier(ID) 를 지정해 주면 되는데, 이제 이 ID로 셀을 관리하게 된다. 본격적으로 컬렉션 뷰(Collection View)의 셀(Cell)을 꾸밀 수 있다.

포스팅에 사용된 앱은 가볍게 영화 포스터를 사진첩 리스트로 만들어 보여주고, 각 항목을 선택했을 때 영화 내용을 자세히 볼 수 있는 화면으로 이동하도록 구성했다.
각 뷰(View)를 객체로 만들기
상태가 변하지 않는 뷰(View)라면 아이디를 부여하지 않아도 되지만, 그게 아니라면 어떤 뷰를 바꿀 것인지 시스템에 알려줘야한다. 먼저 셀(Cell)을 관리할 별도의 클래스를 만든 후 클래스를 지정하고 배치한 각 뷰들을 끌어와 클래스 안에 객체로 만들어준다.

MVVM 패턴으로 데이터 만들기
MVVM 디자인 패턴은 데이터 및 뷰를 각각 독립적으로 관리해서 유지보수가 편하다. 또한 각 부분이 독립적으로 나누어져 있기에 직관적이기도 하다.
각각 Model 과 ViewModel 코드로서 뷰 컨트롤러안에 만들어도 좋고, 각각 파일을 나누어 작성해도 되는데, 나누어서 작성하는 것이 확실히 보기도 깔끔하다.
//Model - 데이터의 구조
struct MovieInfo {
let title: String //영화제목
let openDate: String //개봉일
let numOfTheater: Int //관객수
let poster: String //포스터 이미지 파일명
init(title: String, openDate: String, theater: Int, poster: String) {
self.title = title
self.openDate = openDate
self.numOfTheater = theater
self.poster = poster
}
}
//ViewModel - View 는 무조근 이 ViewModel을 통해 데이터에 접근
class MovieViewModel {
let movies: [MovieInfo] = [
MovieInfo(
title: "아이언맨", //영화제목
openDate: "2008.04.30", //개봉일
theater: 430, //관객수
poster: "ironman.png"), //포스터 파일명
MovieInfo(
title: "광해",
openDate: "2012.09.13",
theater: 1232,
poster: "king.png"),
MovieInfo(
title: "반지의 제왕",
openDate: "2003.12.17",
theater: 75,
poster: "kingOftheRing.png"),
MovieInfo(
title: "기생충",
openDate: "2019.05.30",
theater: 1031,
poster: "parasite.png"),
MovieInfo(
title: "스타워즈",
openDate: "2017.12.14",
theater: 95,
poster: "starwars.png"),
MovieInfo(
title: "트랜스포머",
openDate: "2014.06.25",
theater: 529,
poster: "transformer.png")
]
var listCount:Int {
return movies.count
}
func getTheMovie(at idx:Int) -> MovieInfo {
return movies[idx]
}
}
컬렉션 뷰의 프로토콜(DataSource, Delegate)구현하기
UICollectionViewDataSource 및 UICollectionViewDelegate 구현해야 한다.
class ListViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
}
.
.
.
}
그런데 사실 보기 좀 지저분하다. 그래서 컬렉션 뷰(Collection View)로 리스트를 구현할 때는 각각의 프로토콜을 확장해서 쓰는 방식이 일반적이다 extension 키워드를 사용해서 구현한다.
확장(extension)은 말 그대로 클래스의 기능을 '확장' 한다는 의미이다. 아래와 같이 구현할 수 있다.
class ListViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension ListViewController: UICollectionViewDataSource {
//UICollectionView 의 DataSource 메서드를 따로 구현
}
extension ListViewController: UICollectionViewDelegate {
//UICollectionView 의 Delegate 메서드를 따로 구현
}
훨씬 보기 좋다. 자, 이제 이 방법으로 각각의 메서드를 구현해 볼 것이다.
먼저 상단에 아까 만들었던 뷰모델 객체를 선언한다. 이제 모든 데이터는 이 뷰모델 객체를 통해 접근하게 될 것이다.
class ListViewController: UIViewController {
let viewModel = MovieViewModel() //뷰모델 객체를 선언
override func viewDidLoad() {
super.viewDidLoad()
}
}
DataSource 메서드에 리스트의 갯수와 항목들을 어떻게 보여줄 것인지 구현한다.
//DataSource
extension ListViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return viewModel.listCount //리스트 항목의 총 갯수
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "cell", for: indexPath) as? ListCell else {
//스토리보드에서 ID "cell"을 못찾거나 ListCell 클래스 확인이 안될 땐 기본값 반환
return UICollectionViewCell()
}
let movie = viewModel.getTheMovie(at: indexPath.item)
cell.updateView(info: movie)
return cell
}
}
다음은 Delegate 메서드에 각 항목을 터치했을 때 무엇을 할 것인지 구현한다.
extension ListViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: "show", sender: indexPath.item)
}
}
이제 앱을 실행해보자.

이상하다. 분명 사진첩, 격자 모양의 리스틀 기대했는데... 이는 각 항목의 크기가 전체를 잡아먹었기 때문에 생긴 일이다. 컬렉션 뷰에서는 각 항목의 크기를 코드로 작성해 주어야 한다. 항목의 크기를 화면 넓이 / 2 의 크기로 정해주자.
각 항목의 크기 정하기
사실 우리가 그리드 형식으로 구현하고 싶다고 해도, 시스템은 어떤 크기로 어떻게 보여줘야할 지 알 길이 없다. 그래서 직접 항목의 크기를 정해주어야 한다.
이 역시 컬렉션 뷰(Collection View)에서 메소드를 제공하니 아래와 같이 구현해주면 된다.
extension ListViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemSpacing: CGFloat = 10 //항목의 간격
let textAreaHeight: CGFloat = 35 //글자가 들어간 높이
//가로 크기 (총 너비 - 간격)/(한 줄에 들어갈 갯수)
let width: CGFloat = (collectionView.bounds.width - itemSpacing)/2
//세로 크기 (가로길이 * 비율 + 글자 높이)
let height: CGFloat = width * 10/7 + textAreaHeight
return CGSize(width: width, height: height)
}
}
그리고 이렇게 직접 크기를 정해줄 때는 스토리보드에서 컬렉션 뷰의 속성을 수정해야한다.

이제 다시 한번 돌려보자. 원하는 레이아웃으로 나오는 것을 확인할 수 있다.
