[Swift] Youtube API 활용하여 앱 만들기 (3) : 무한스크롤뷰 오류 해결, search API 설정

Oni·2023년 9월 6일
0

TIL

목록 보기
41/47
post-thumbnail

무한 스크롤뷰 오류

어제 짠 코드 중에 스크롤을 내리면 똑같은 영상들이 계속 주르륵 나오는 오류가 있었다.
일단 뭐가 어디서부터 잘못됐는지 확인하기 위해 프린트문으로 확인하거나, 브레이크 포인트를 둬서 디버깅을 시도했다.

추정되는 원인은 api로 불러오는 데이터는 양이 많아서 한 페이지에 5개의 영상만 불러와지는데, 다음 페이지를 토큰으로 불러오는데 계속해서 여러번 호출하는 문제다.
중간에 프린트문으로 nextPageToken을 출력하는데 다 로드되지도 않았는데 출력문이 5~6번 계속 나오고 실제로 로드된 영상도 그만큼 반복하는 것을 볼 수 있었다.

해결방안

API를 로드하는 로직에 isLoadingData라는 Bool 형태의 프로퍼티를 생성하여, 한 페이지에서 데이터를 가져올 동안에 다시 로드할 수 없게끔 처리했다.

// MARK: - Variables
// API 호출여부 확인하는 프로퍼티 생성
private var isLoadingData = false

// MARK: - YouTube Video Load
private func loadVideo(pageToken: String? = nil) {
	// API 호출중이 아닐때 아래 코드 진행
	guard !isLoadingData else { return }
	isLoadingData = true
	APIManager.shared.fetchVideos(pageToken: nextPageToken ?? "") { [weak self] result in
		switch result {
        
        // API 데이터 로드 로직
        
        }
        self?.isLoadingData = false
    }
}

이렇게 되면 처음 화면이 로드될 때 5개만 보이고, 스크롤을 내리면 추가로 5개, 또 내리면 5개... 이런식으로 무한 스크롤 시 데이터를 로드하여 화면에 띄울 수 있게 된다.

🤳🏻 적용화면


search API 설정

API Manager

어제 로드한 API는 video에 대한 정보이고 오늘은 search를 구현하려고 한다. 유튜브 API 가이드를 살펴보면 데이터의 형태가 조금 다른데, fetchVideo 함수를 그대로 쓸 수 없어서 아래와 같이 작성했다.

func searchFetchVideos(pageToken: String, completion: @escaping (Result<Any, AFError>) -> Void) {
	let url = API.baseUrl + "search"
	let apiParam = [
		"part": "snippet",
		"key": API.key,
		"q": SearchViewController.searchText,
		"pageToken": "\(pageToken)"
	] as [String: Any]
        
	AF.request(url, method: .get, parameters: apiParam)
    	.validate()
        .responseJSON { response in
        	completion(response.result)
			debugPrint(response)
        }
}

여기서 q는 쿼리를 뜻하며, 검색어를 의미한다.
해당 쿼리는 SearchVC에 searchBar에 입력된 텍스트를 받아 검색을 하게 된다.

loadVideo()

videoLoad 로직과 videiId와 thumbnail만 차이가 있어서 해당 부분만 수정했고, 셀은 HomeVC에서 만든 셀을 그대로 재사용했다.

// MARK: - YouTube Video Load
private func loadVideo(pageToken: String? = nil) {
    guard !isLoadingData else { return }
    isLoadingData = true
	APIManager.shared.fetchVideos(pageToken: nextPageToken ?? "") { [weak self] result in
		switch result {
		case .success(let data):
			if let json = data as? [String:Any],
            let items = json["items"] as? [[String:Any]] {
            	for item in items {
                	if let videoId = item["id"] as? [String:Any],
                       let id = videoId["videoId"] as? String,
                       !SearchViewController.videoIds.contains(id),
                       let snippet = item["snippet"] as? [String:Any],
                       let title = snippet["title"] as? String,
                       let thumbnails = snippet["thumbnails"] as? [String:Any],
                       let medium = thumbnails["medium"] as? [String:Any],
                       let thumbnailUrl = medium["url"] as? String,
                       let user = snippet["channelTitle"] as? String {
                		AF.request(thumbnailUrl).responseData { response in
                        	switch response.result {
                            case .success(let data):
                            	if let image = UIImage(data: data) {
                                	SearchViewController.videoIds.append(id)
                                    self?.thumbnails.append(image)
                                    self?.titles.append(title)
                                    self?.users.append(user)
                                    DispatchQueue.main.async {
                                    	self?.collectionView.reloadData()
                                    }
                            	} else {
                                	print("Failed to convert data to UIImage")
                                }
                            case .failure(let error):
                            print("Image download error: \(error)")
                		}
                       }
            	}
            }
            self?.nextPageToken = json["nextPageToken"] as? String
            self?.loadVideo(pageToken: self?.nextPageToken)
		}
		case .failure(let error):
        print(error)
		}
        self?.isLoadingData = false
	}
}

🤳🏻 적용화면

profile
하지만 나는 끝까지 살아남을 거야!

1개의 댓글

comment-user-thumbnail
2023년 9월 7일

Search 부분까지 api를 활용하셨네요 훌륭합니다!

답글 달기