본문 바로가기

iOS 개발/Unsplash 클론 코딩

Unsplash - 14. Search Result 화면

반응형

안녕하세요 Skillist입니다.

 

SearchMainViewController에서 결과 화면을 지정한것 기억하시나요

바로 이부분이에요.

 

이번엔 검색 결과 화면인 SearchResultViewController을 보겠습니다.

12라인 : collectionView를 선언합니다. 레이아웃은 flowlayout입니다.

또한, cell들을 등록합니다.

 

28라인 : scope 버튼 변경 시 동작합니다. collectionView를 top으로 이동하고, 변경된 scope에 대한 검색을 수행해요.

 

43라인 : ViewDidLoad에서 레이아웃을 구성합니다.

 

60라인 : section은 1개로 구현할거에요.

 

64라인 : items 개수를 리턴합니다.

 

68라인 : 현재 설정된 scope Type에 따라, cell을 리턴할거에요. 

 

70라인 : Type에 따른 분기를 태울겁니다.

 

71라인 : Photos 검색 시 Photos cell을 반환합니다.

 

89라인 : collection 검색 시, collection cell을 반환합니다.

 

107라인 : user 검색 시, user Cell을 반환합니다.

 

124라인 : cell row에 따라, 레이아웃을 다르게 가져가기 위해 분기를 탑니다. 아래 그림을 보시죠.

첫번째 row인 1번은 위쪽 모서리를 round 처리 했습니다.

중간 row인 2번은 모서리 처리를 하지 않았습니다.

마지막 row인 3번은 아래쪽 모서리를 round 처리했습니다.

구현 방법은 다양하겠지만, 저는 row값으로 모서리 처리로 구현했어요.

 

다음은 네트워크 처리 입니다.

138라인 : 검색 결과값을 리셋하죠. searchBar의 cancel버튼을 터치하거나 새로 검색 수행시 호출됩니다.

 

143라인 : 검색을 수행하거나, scope 버튼을 터치했을때 수행합니다. 매개변수인 searchText의 기본값을 "" 값으로 설정했어요.

검색 버튼을 터치하여 검색 수행시, 입력한 값을 넘겨주고,

scope 버튼만 터치하여 검색 시엔, 기존에 입력한 searchText를 기반으로, 검색하도록 합니다.

 

149라인 : 기존 검색 중인 task를 종료하고 신규 검색을 수행해요. 예를 들어, photos에 대한 검색 도중 users에 대한 검색을 시작하면 기존 photos 검색은 종료해야겠죠???

 

157라인 : 현재 페이지 번호가, 마지막 페이지 번호라면 검색을 수행하지 않습니다.

 

161라인 : prefetch에서, 중복 페이지 로드 방지를 위한 로직이에요. 중복 페이지 로드라면, 수행하지 않습니다.

181라인 : 검색 결과에 따른, type으로 분기를 탑니다. 중복코드가 보이죠???? 수정해볼게요

중복 코드를 싹 정리했습니다.

글 작성하면서 코드를 다시 한번더 볼수 있고, 정리할수 있어서 좋네요.~ 👍🏻

메인쓰레드에서 collectionView를 리로드 합니다.

 

 

다음은 UICollectionViewDelegateFlowLayout에 대한 구현인데요, 역시 검색 타입에 따라 분기를 탑니다.

타입에 맞게 사이즈를 설정했어요.

 

이번엔 스크롤시 페이지 로딩 구현 부인 prefetch입니다.

다른 VC에서도 사용하는 로직이에요. 남아있는 cell의 개수가 3개인 경우에는 items.count -11, items.count -6에서 걸러내지 못하겠죠? 그래서, 여러 cell row에 대해 수행을 하고, 중복 수행이 되지 않도록 "isSearchFetching" 저장 프로퍼티로 적절히 로직을 구현했습니다.

 

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

 

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

class SearchResultViewController: UIViewController {
    //메인 사진 목록을 보여줄 CollectionView
    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 1
        layout.minimumInteritemSpacing = 0
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.prefetchDataSource = self
        collectionView.register(PhotoListCollectionViewCell.self, forCellWithReuseIdentifier: "PhotoListCollectionViewCell")
        collectionView.register(SearchCollectionCollectionViewCell.self, forCellWithReuseIdentifier: "SearchCollectionCollectionViewCell")
        collectionView.register(SearchUserCollectionViewCell.self, forCellWithReuseIdentifier: "SearchUserCollectionViewCell")
        collectionView.backgroundColor = .black
        return collectionView
    }()
    
    var currentSearchType: SearchType = .Photos {
        didSet {
            //scopeButton을 변경하면, collectionView의 스크롤을 top으로 이동
            collectionView.setContentOffset(.zero, animated: false)
            //검색
            fetchFirstSearch()
        }
    }
    
    private var isSearchFetching = false
    private var searchPageNum = 0
    private var searchLastPageNum = 0
    var searchText = ""
    var items: [Any] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupCollectionView()
    }
    
    private func setupCollectionView() {
        view.addSubview(collectionView)
        
        collectionView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
}

extension SearchResultViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        switch currentSearchType {
        case .Photos:
            guard let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: "PhotoListCollectionViewCell",
                for: indexPath
            ) as? PhotoListCollectionViewCell else {
                return UICollectionViewCell()
            }
            
            if items.count - 1 < indexPath.row {
                return UICollectionViewCell()
            }
            
            guard let item = items[indexPath.row] as? Photo else {
                return UICollectionViewCell()
            }
            cell.setup(photo: item)
            return cell
            
        case .Collections:
            guard let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: "SearchCollectionCollectionViewCell",
                for: indexPath
            ) as? SearchCollectionCollectionViewCell else {
                return UICollectionViewCell()
            }
            
            if items.count - 1 < indexPath.row {
                return UICollectionViewCell()
            }
            
            guard let item = items[indexPath.row] as? Collection else {
                return UICollectionViewCell()
            }
            cell.setup(collection: item)
            return cell
            
        case .Users:
            guard let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: "SearchUserCollectionViewCell",
                for: indexPath
            ) as? SearchUserCollectionViewCell else {
                return UICollectionViewCell()
            }
            
            let row = indexPath.row
            if items.count - 1 < row {
                return UICollectionViewCell()
            }
            
            guard let item = items[row] as? User else {
                return UICollectionViewCell()
            }
            
            if row == 0 {
                cell.firstIndexSetup(user: item)
            } else if row == items.count-1 {
                cell.lastIndexSetup(user: item)
            } else {
                cell.middleIndexSetup(user: item)
            }
            return cell
        }
    }
}

extension SearchResultViewController {
    
    func resetResult() {
        items = []
        collectionView.reloadData()
    }
    
    func fetchFirstSearch(searchText: String = "") {
        resetResult()
        
        if !searchText.isEmpty {
            self.searchText = searchText
        }
        UnsplashAPI.dataTask?.cancel()
        isSearchFetching = false
        searchPageNum = 0
        searchLastPageNum = -1
        fetchSearch()
    }
    
    func fetchSearch() {
        if searchPageNum == searchLastPageNum {
            return
        }
        
        if isSearchFetching {
            return
        }
        isSearchFetching = true
        
        UnsplashAPI.fetchSearchResult(searchText: searchText, searchType: currentSearchType, pageNum: searchPageNum + 1) { [weak self] data, response, error in
            self?.isSearchFetching = 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 {
                    var results: [Any] = []
                    switch self?.currentSearchType {
                    case .Photos:
                        let result = try JSONDecoder().decode(SearchPhotos.self, from: data)
                        self?.searchLastPageNum = result.totalPages
                        results = result.results
                        
                    case .Collections:
                        let result = try JSONDecoder().decode(SearchCollections.self, from: data)
                        self?.searchLastPageNum = result.totalPages
                        results = result.results
                        
                    case .Users:
                        let result = try JSONDecoder().decode(SearchUsers.self, from: data)
                        self?.searchLastPageNum = result.totalPages
                        results = result.results
                        
                    case .none:
                        return
                    }
                    
                    if self?.searchPageNum == 0 { //첫페이지를 가져온 경우 목록 설정
                        self?.items = results
                    } else { //첫페이지 외 다음페이지를 가져온 경우 목록 설정
                        self?.items.append(contentsOf: results)
                    }
                    
                    //다음 페이지 번호 설정
                    self?.searchPageNum += 1
                    DispatchQueue.main.async {
                        self?.collectionView.layoutIfNeeded()
                        self?.collectionView.reloadData()
                    }
                } catch {
                    DispatchQueue.main.async {  //에러 발생 시 에러 보여주기
                        self?.showNetworkErrorAlert(error: .jsonParsingError)
                    }
                }
                
            default:
                DispatchQueue.main.async {  //에러 발생 시 에러 보여주기
                    self?.showNetworkErrorAlert(error: .networkError)
                }
                return
            }
        }
    }
}

extension SearchResultViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        switch currentSearchType {
        case .Photos:
            guard let photo = items[indexPath.row] as? Photo else {
                return .zero
            }
            let cellWidth = collectionView.frame.width
            let cellHeight = photo.imageRatio * cellWidth
            return CGSize(width: cellWidth, height: cellHeight)
            
        case .Collections:
            let cellWidth = collectionView.frame.width - 10
            let cellHeight = cellWidth * 0.7
            return CGSize(width: cellWidth, height: cellHeight)
            
        case .Users:
            let cellWidth = collectionView.frame.width - 20
            let cellHeight:CGFloat = 100
            return CGSize(width: cellWidth, height: cellHeight)
        }
    }
    
    //여백 설정
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        switch currentSearchType {
        case .Photos:
            return UIEdgeInsets(top: 0.5, left: 0, bottom: 0.5, right: 0)
            
        case .Collections:
            return UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
            
        case .Users:
            return UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
        }
    }
}

extension SearchResultViewController: UICollectionViewDataSourcePrefetching {
    
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        guard let indexPath = indexPaths.first else {
            return
        }
        
        let row = indexPath.row
        if row == items.count - 11 ||
            row == items.count - 6 ||
            row == items.count - 1 {
            fetchSearch()
        }
    }
}
반응형