본문 바로가기

iOS 개발/Unsplash 클론 코딩

Unsplash - 7. 사진 목록 구현하기

반응형

안녕하세요. Skillist에요.

 

이번엔 사진 목록을 보여줄 PhotoListViewcontroller을 알아볼게요.

 

코드를 기능별로 차근차근 알아볼게요

 

12라인 : 현재 호출한 페이지 넘버를 저장합니다.

 

13라인 : 사진 목록을 저장합니다.

 

16라인 : 아주 소중한 CollectionView입니다.

UICollectionViewflowLayout으로, 라인 사이즈 설정하고,  uicollectionView를 생성했습니다.

collectionView에 대한 delegate와 datasource를 self로 설정했습니다.

 

25라인 : 콜렉션뷰를 스크롤 할 때, 남아있는 cell의 개수가 일정 수가 되면, 다음 페이지의 사진을 가져올건데, 이를 prefetchDataSource를 통해 구현합니다.

 

26라인 : collectionView의 cell을 등록합니다.

 

27라인 : 백그라운드컬러를 검정색으로 설정합니다.

 

30라인 : 콜렉션뷰를 아래로 당기면 새로고침하는 기능을 구현하기 위해 선언했습니다.

 

31라인 :  리프레시를 수행할때의 action을 추가했습니다.

 

리프레시 action인데요, pageNum을 0으로 세팅하고, 사진 목록을 가져옵니다.

 

viewDidLoad에서, 레이아웃을 설정하고 사진 목록을 가져옵니다.

51라인 : collectionView를 뷰에 추가합니다.

 

54라인 : SnapKit을 통해 레이아웃을 설정합니다. 아이폰의 상단과 하단(세프티 영역)에도 collectionView를 꽉 채우고 싶어서, 사이즈를 superView에 맞췄습니다.

 

60라인 : 사진 목록을 가져오는 메소드 입니다. "Unsplash - 6. 사진 목록 받아오기"에서 알아봤으니, 생략 할게요.

 

다음은 collectionView에 대한 코드입니다.

137라인 : 현재 사진 목록에 대한 개수를 리턴해주고 있어요.

 

142라인 : cell이 PhotoListCollectionViewCell인 경우, 셀을 사진 정보로 설정하고 리턴해주고 있어요.

 

149라인 : cell을 터치하면, cell을 중앙으로 이동하고, 0.5초 뒤에, 사진에 대한 상세 화면으로 이동합니다.

 

153라인 : 현재 목록을 상세 화면으로 넘겨줘요. 상세화면에서는 좌우 스크롤?을 통해서 사진을 넘길 수 있거든요

 

154라인 : 선택한 사진의 row를 상세 화면으로 넘겨줘, 선택한 사진을 바로 보여줄 수 있어요.

 

155라인 : 상세 화면을 아래로 내려서 사라지게끔 구현할거에요. 그러기 위해서 modalPresentationStyle을 overFullScreen으로 설정해야 해요. 그래야만 기존 화면인 사진 목록VC를 뒤쪽에 보이도록 남길 수 있거든요

 

156라인 : 상세화면에서 좌우 스크롤을 통해 사진을 변경하면, 변경된 이벤트를 받을거에요. 이를 delegate로 구현했어요

 

157라인 : 상세 화면을 보여줄거에요.

 

다음은 cell size 설정이에요.

172, 173라인 : 사진의 가로, 세로 사이즈를 API를 통해서 받아와요. 이를 현재 아이폰의 가로 사이즈로 계산하여 사진의 height를 계산해요. 결과적으로 아이폰 가로 사이즈를 기준으로 cell의 height가 설정돼요. 사진마다 다양한 height가 구현되겠죠?

 

178라인 : 사진의 상,하단의 여백을 0.5로 설정했어요.

 

25라인에서 설명한 다음 페이지 미리 로딩하는 코드에요.

prefetching에 대한 설명이에요.

셀에 대한 데이터 소스에 대한 로드를 시작할때 호출합니다. 쉽게 말하면, 다음 셀을 로드할때 호출합니다.

따라서, 우리는 스크롤 하면서, 어떤 셀들이 준비중인지 알 수 있어요. 이를 활용할거에요.

 

186라인 : 현재 사진 목록의 개수가 0일때, 아무런 작업 하지 않습니다. 목록을 호출하거나 비정상적인 상황이라고 봐야겠죠.

 

190라인 : 인덱스 배열을 받을 수 있는데, first만 활용할거에요. 인덱스 배열이 비어있을때는 종료할거에요. "indexPaths.first" 보다는 "indexPaths.isEmpty"가 더 어울리겠네요.

 

195라인 : 현재 row가 사진 목록 중 마지막 10번째 이거나, 5번째, 0번째 일때, 다음 페이지를 로딩할겁니다.

생각해 보니, 인터넷이 느리고, 스크롤이 빨라서, 10번째와 5번째, 0번째를 한번에 보여줄 경우 다음 페이지 로딩에 대한 문제가 발생하겠네요. 이부분은 수정을 해야겠어요.

 

현재 사진 목록을 가져오는중인지 확인하는 값을 추가하였습니다. fetchPhotos에서 isFetching을 확인합니다.

업데이트 중이라면, 다음 페이지를 가져오는 중이니, 종료하구요. 그게 아니라면 업데이트를 수행합니다.

그렇다면 195라인에서 fetchPhotos를 여러번 호출하여도 문제 없겠죠?

 

다음은 스크롤 시 TabBar 숨기기 로직입니다.

118라인 : velocity를 받아올 수 있습니다. 스크롤 속도를 받아올 수 있죠.

 

120라인 : 우리는 스크롤이 x 방향으로는 안되기 때문에, y만 봅니다.

 

127라인 : y에 대한 속도를 확인해서, tabbar의 alpha와 origin을 변경합니다.

해당 로직으로 스크롤 방향에 따라 TabBar의 hidden 여부가 결정됩니다.

 

137라인 : VC의 StatusBar 컬러를 밝은 계열로 설정했습니다.

 

이렇게 PhotoListViewController에 대해서 알아봤습니다.

 

 

----------------------------------------------------------------------------------------------------------------

 

하나 질문이 있는데요,

157라인에서 cell을 선택할 경우 cell을 화면 중앙으로 이동한다고 말씀드렸습니다.

하지만, 중앙으로 이동한 cell이 디스플레이의 중앙이 아니더라구요.

중앙보다 살짝 위로 더 올라가 있어요. 

이를 해결하고자, 구글링, view 하이라키도 보고, 숨겨진 여백이 있는지도 보고, tabbar를 제거해보기도 했는데,

원인을 찾지 못했고 문제를 해결하지도 못했습니다.

이슈에 대한 이유를 아신다면 댓글로 꼭 좀 부탁드리겠습니다.

 

잘못되거나 부족한 내용 등, 피드백 감사합니다!

 

Skillist의 Unsplash 프로젝트

https://github.com/DeveloperSkillist/UnsplashCloneCode

 

GitHub - DeveloperSkillist/UnsplashCloneCode: UnsplashCloneCode

UnsplashCloneCode. Contribute to DeveloperSkillist/UnsplashCloneCode development by creating an account on GitHub.

github.com

 

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓  전체 코드  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

import UIKit
import SnapKit

class PhotoListViewController: UIViewController {
    private var pageNum = 0
    private var photos: [Photo] = []
    private var isFetching = false
    
    //메인 사진 목록을 보여줄 CollectionView
    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 1
        layout.minimumInteritemSpacing = 0
        layout.invalidateLayout()
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.prefetchDataSource = self
        collectionView.register(PhotoListCollectionViewCell.self, forCellWithReuseIdentifier: "PhotoListCollectionViewCell")
        collectionView.backgroundColor = .black
        
        //스크롤 다운하여 목록 리프레시 구현
        let refreshController = UIRefreshControl()
        refreshController.addTarget(self, action: #selector(refreshPhotos), for: .valueChanged)
        collectionView.refreshControl = refreshController
        return collectionView
    }()
    
    //스크롤 다운하여 목록 리프레시 시도 시 수행할 동작
    @objc func refreshPhotos() {
        pageNum = 0
        fetchPhotos(isRefresh: true)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        setupLayout()
        fetchPhotos()
    }
    
    //레이아웃 구현
    private func setupLayout() {
        view.addSubview(collectionView)
        
        view.backgroundColor = .black
        collectionView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
    
    //이미지 fetch 구현
    private func fetchPhotos(isRefresh: Bool = false) {
        if isFetching {
            return
        }
        isFetching = true
        
        //현재 페이지에 1을 더하여 다음 페이지 가져오기
        UnsplashAPI.fetchPhotos(pageNum: pageNum + 1) { [weak self] data, response, error in
            self?.isFetching = false
            guard error == nil,
                  let response = response as? HTTPURLResponse,
                  let data = data else {
                      DispatchQueue.main.async {    //에러 발생 시 에러 보여주기
                          self?.showNetworkErrorAlert(error: .networkError)
                      }
                      return
                  }
            
            switch response.statusCode {
            //response 성공 시, 목록 설정하기
            case (200...299):
                do {
                    let fetchedPhotos = try JSONDecoder().decode([Photo].self, from: data)
                    
                    
                    if self?.pageNum == 0 { //첫페이지를 가져온 경우 목록 설정
                        self?.photos = fetchedPhotos
                    } else { //첫페이지 외 다음페이지를 가져온 경우 목록 설정
                        self?.photos.append(contentsOf: fetchedPhotos)
                    }
                    
                    DispatchQueue.main.async {
                        //리프레시 한 경우 refreshControl 종료
                        if isRefresh {
                            self?.collectionView.refreshControl?.endRefreshing()
                        }
                        
                        //다음 페이지 번호 설정
                        self?.pageNum += 1
                        self?.collectionView.reloadData()
                    }
                } catch {
                    DispatchQueue.main.async {  //에러 발생 시 에러 보여주기
                        self?.showNetworkErrorAlert(error: .jsonParsingError)
                    }
                }
                
            default:
                DispatchQueue.main.async {  //에러 발생 시 에러 보여주기
                    self?.showNetworkErrorAlert(error: .networkError)
                }
                return
            }
        }
    }
    
    //scroll 시 tabbar show or hide 구현
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        UIView.animate(withDuration: 0.3, animations: { [weak self] in
            let velocityY = velocity.y
            guard velocityY != 0 else {
                return
            }
            
            var tabbarHeight: CGFloat = UIScreen.main.bounds.maxY
            var tabBarAlpha: CGFloat = 0
            if velocityY < 0 { //최상단의 사진을 향해 스크롤 중
                tabbarHeight -= self?.tabBarController?.tabBar.frame.height ?? 0
                tabBarAlpha = 1
            }
            
            self?.tabBarController?.tabBar.alpha = tabBarAlpha
            self?.tabBarController?.tabBar.frame.origin = CGPoint(x: 0, y: tabbarHeight)
        })
    }
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
}

extension PhotoListViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return photos.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        //cell 설정
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoListCollectionViewCell", for: indexPath) as? PhotoListCollectionViewCell else {
            return UICollectionViewCell()
        }
        cell.setup(photo: photos[indexPath.row])
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            let detailVC = PhotoDetailViewController()
            detailVC.photos = self?.photos ?? []
            detailVC.startRow = indexPath.row
            detailVC.modalPresentationStyle = .overFullScreen   //pullDown하여 VC 종료를 구현하기 위해 overFullScreen 구현
            detailVC.cellChangeDelegate = self  //PhotoDetailViewController에서 사진 변경 시, 목록 위치를 변경하기 위하여 Delegate 구현
            self?.present(detailVC, animated: false, completion: nil)
        }
    }
}

extension PhotoListViewController: CellChangeDelegate {
    func changedCell(row: Int) {    //PhotoDetailViewController에서 사진 변경 시, 메인 목록의 위치를 변경
        collectionView.scrollToItem(at: IndexPath(row: row, section: 0), at: .centeredVertically, animated: false)
    }
}

extension PhotoListViewController: UICollectionViewDelegateFlowLayout {
    //collectionView의 item 사이즈 구현
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let photo = photos[indexPath.row]
        let cellWidth = collectionView.frame.width
        let cellHeight = photo.imageRatio * cellWidth
        return CGSize(width: cellWidth, height: cellHeight)
    }
    
    //여백 설정
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0.5, left: 0, bottom: 0.5, right: 0)
    }
}

extension PhotoListViewController: UICollectionViewDataSourcePrefetching {
    //스크롤 시 마지막 사진이 보여지기 전에 다음 페이지 미리 로딩하기
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        if photos.count == 0 {
            return
        }
        
        guard let row = indexPaths.first?.row else {
            return
        }
        
        if row == photos.count - 11 || row == photos.count - 6 || row == photos.count - 1 {
            fetchPhotos()
        }
    }
}
반응형