상상하라 그리고 현실로 만들어라.

상상하는 모든 것이 미래다.

Swift와 iPhone/iPhone App

스위프트 아이폰 앱 리스트 컬렉션 뷰(CollectionView)로 사진첩 리스트 만들기

월터제이(Walter J) 2021. 4. 16. 16:43

아이폰 앱 리스트 만들기

컬렉션 뷰(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)
    }
}

 

  그리고 이렇게 직접 크기를 정해줄 때는 스토리보드에서 컬렉션 뷰의 속성을 수정해야한다.

 

 

 

 

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

 

 

 

반응형