[TIL] iOS 심화 주차 과제 : 포켓몬 도감 앱 만들기 day.01

Emily·2024년 12월 30일
2

PictorialBookApp

목록 보기
1/5

포켓몬 도감 앱의 주요 구현 사항은 UICollectionView, REST API, RxSwift다. 그리고 이 내용들을 MVVM 아키텍처에 담아야 한다. 가이드에는 ModelViewModel부터 구현하도록 되어있지만, 나는 View부터 구현하였다.

Level 4

MainViewController 구현

  • UIImageView : 상단 중앙에 포켓몬 볼 이미지 구성
  • UICollectionView : 세로 스크롤. 한 줄에 3개의 포켓몬 이미지로 구성된 그리드

우선 컨테이너 뷰인 MainView 객체를 생성한 뒤 MainViewControllersafeArea에 채워주었다.

class MainViewController: UIViewController {
    
    private lazy var containerView: MainView = .init()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupNavigationBar()
        addSubviews()
        layout()
    }
    
    private func setupNavigationBar() {
        navigationController?.navigationBar.isHidden = true
    }
    
    private func addSubviews() {
        view.addSubviews([containerView])
    }

    private func layout() {
        view.backgroundColor = .mainRed
        
        let safeArea = view.safeAreaLayoutGuide
        
        containerView.snp.makeConstraints {
            $0.edges.equalTo(safeArea)
        }
    }
}

UIImageView

MainView의 구성은 상단 가운데에 있는 포켓몬 볼 이미지와 포켓몬 이미지 컬렉션 뷰다. 이미지는 화면 너비의 1/3 크기의 정사각형으로 지정하고, 컬렉션 뷰는 내용 없이 우선 영역만 잡아주었다.

class MainView: UIView {
	private lazy var pokemonBallImageView: UIImageView = {
        let imageView = UIImageView()
        
        imageView.image = .pokemonBall
        imageView.contentMode = .scaleAspectFit
        
        return imageView
    }()
    
    // collection view : 영역 지정을 위해 초기화만 구현
    private lazy var pokemonCollectionView: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
        collectionView.backgroundColor = .darkRed
        return collectionView
    }()
    
    // ... //
}
private func layout() {
	pokemonBallImageView.snp.makeConstraints {
		$0.centerX.equalToSuperview()
		$0.top.equalToSuperview().offset(10)
		$0.width.equalToSuperview().dividedBy(3)	// 화면 너비의 1/3 크기로 지정
		$0.height.equalTo(pokemonBallImageView.snp.width)	// 높이를 너비와 같게 하여 정사각형 영역으로 구현
    }
        
    pokemonCollectionView.snp.makeConstraints {
        $0.top.equalTo(pokemonBallImageView.snp.bottom).offset(10)
        $0.horizontalEdges.bottom.equalToSuperview()
    }
}

포켓몬 볼 이미지 뷰 영역과 컬렉션 뷰 영역의 레이아웃이 잘 적용된 모습

UICollectionViewCompositionalLayout

지금까지는 UICollectionViewFlowLayout을 통해 컬렉션 뷰의 레이아웃을 구성했지만, 이번에는 CompositionalLayout을 사용해보려 한다. 컴포지셔널 레이아웃은 구성 요소의 계층 별 레이아웃을 지정해주어야 한다.

여기서 기존에 없던 group이라는 개념이 등장한다. 우선 각 셀을 item이라고 생각하면 쉽다. item이 모여 group을 이루고, group이 모여 section을 이룬다. sectionheaderfooter 같은 supplementary view를 가질 수 있는 단위다.

groupsection의 차이가 뭘까 헷갈릴 수 있는데, 나도 구현을 하면서 실감한 개념이다. vertical 스크롤을 가진 컬렉션 뷰의 경우 하나의 행을 이루는 단위, horizontal 스크롤을 가질 경우에는 하나의 열을 이루는 단위라고 생각하면 쉽다.

코드로 CompositionalLayout을 구현하면 아래와 같다.

private var collectionViewLayout: UICollectionViewLayout {
	// 셀 하나의 크기 : 너비는 컬렉션 뷰 너비의 1/3, 높이는 너비와 같게 지정
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalWidth(1/3))
  	
    // 정의한 itemSize를 갖는 item 선언
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    
    // item 영역의 구성요소(이미지 뷰)에 inset 지정
    item.contentInsets = .init(top: 8, leading: 8, bottom: 8, trailing: 8)
    
    // 한 행의 크기 : 너비는 컬렉션 뷰 너비와 같게, 높이는 너비의 1/3 만큼으로(item 너비=높이와 같게) 지정
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(1/3))
   	
    // 정의한 groupSize를 갖는 group 선언하고 구성하는 item 개수와 방향 지정
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 3)
    
    // 정의한 group으로 이루어진 section 선언
    let section = NSCollectionLayoutSection(group: group)
    
    // 해당 section을 갖는 layout 반환
    return UICollectionViewCompositionalLayout(section: section)
}

MainView에 선언했던 pokemonCollectionView에 이렇게 정의한 레이아웃을 적용해주고, 이미지 뷰로 꽉 찬 PokemonCell을 정의한 뒤 register 해주었다. 나중에 API 통신으로 불러온 포켓몬 이미지들을 적용해야 하지만, 임시로 포켓몬 볼 이미지를 넣고 레이아웃이 잘 적용되었는지 확인해보았다.

한 행에 3개의 이미지가 들어간 그리드 형태로 컬렉션 뷰 레이아웃이 잘 적용된 모습

Level 5

UICollectionViewCell을 탭했을 때 화면 전환
셀을 탭 했을 때 DetailViewController로 이동하도록 구현

각 셀이 포켓몬 정보를 갖고 있다는 전제 하에, 해당 포켓몬에 대한 디테일이 DetailView에 그려지며 이동해야 하지만 나는 아직 데이터 fetchbind를 구현하지 않았기 때문에 그냥 이동 동작만 구현했다. UICollectionViewDelegate를 사용하여 구현했는데, 컨테이너 뷰 객체에 채택시키지 않고 MainViewController에 채택하여 구현했다.

class MainView: UIView {
	// ... //
    private lazy var pokemonCollectionView: UICollectionView = {
    	// ... //
    }()
    
    // collection view의 delegate를 외부(controller)에서 할당
    func setDelegate(_ delegate: UICollectionViewDelegate) {
        pokemonCollectionView.delegate = delegate
    }
}

class MainViewController: UIViewController {
	override func viewDidLoad() {
        super.viewDidLoad()
        // 컨테이너 뷰의 컬렉션 뷰 delegate에 controller가 self를 할당
        containerView.setDelegate(self)
    }
}
extension MainViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let vc = DetailViewController()
        navigationController?.pushViewController(vc, animated: true)
    }
}

네비게이션 바의 경우, MainView에는 없고 DetailView에는(Back 버튼) 있다. 이 때 네비게이션 바의 isHidden 값은 컨트롤러마다 viewDidLoad에 설정을 해주어도 이동 직전 화면의 설정에 그대로 영향을 받는 것을 경험했다.

MainViewControllernavigationBar.isHidden = true를 했지만 navigationBar.isHidden = falseDetailViewController에 갔다오자 다시 네비게이션 바 영역이 생긴 모습

그래서 navigationBar.isHidden 설정 코드를 양쪽 컨트롤러의 viewWillAppear에 옮겨주었다.

Level 7

DetailViewController 구현

  • MainView의 컬렉션 뷰 셀을 누르면 해당 포켓몬의 상세 정보 화면(DetailView)으로 이동
    하지만 아직 데이터 fetch 구현 전이므로 UI 작업만 함.

UIStackView

처음에 그냥 단순하게 DetailView라는 DetailViewController의 컨테이너 뷰에 컴포넌트들을 넣고 top에 대한 제약조건을 넣으면서 컴포넌트 간 간격을 잡았었는데, 컴포넌트의 박스 뷰가 컨테이너 뷰의 가운데에 오도록 구현하고 싶어서 subview로 스택 뷰를 한 번 더 정의하고, 그 안에 컴포넌트들을 넣었다. 그리고 네비게이션 바의 Back 버튼이 파란색인 게 별로 안이뻐보여서 tintColor를 흰색으로 지정했다.

class DetailStackView: UIStackView {
    
    private lazy var pokemonImageView: UIImageView = {
        let imageView = UIImageView()
        
        imageView.contentMode = .scaleAspectFit
        
        return imageView
    }()
    
    private lazy var nameLabel: UILabel = {
        let label = UILabel()
        
        label.font = .systemFont(ofSize: 24, weight: .bold)
        label.textColor = .white
        label.textAlignment = .center
        
        return label
    }()
    
    private lazy var typeLabel = detailLabel()
    
    private lazy var heightLabel = detailLabel()
    
    private lazy var weightLabel = detailLabel()
    
    // 재사용 label
    private func detailLabel() -> UILabel {
        let label = UILabel()
        
        label.font = .systemFont(ofSize: 18)
        label.textColor = .white
        label.textAlignment = .center
        
        return label
    }
    
    // ... //
    
    private func layout() {
        backgroundColor = .darkRed
        layer.cornerRadius = 15
        axis = .vertical
        spacing = 8
        distribution = .fill
        
        pokemonImageView.snp.makeConstraints {
            $0.height.equalToSuperview().dividedBy(2)
        }
    }
}

임시로 포켓몬 볼 이미지 + 고라파덕의 정보를 넣고 구현한 DetailView UI
profile
iOS Junior Developer

0개의 댓글