/////
Search
Duplicate

Join Us

생성일
2022/08/28 15:12
태그
개인 프로젝트
기간
2021.10 ~ 2022.02
언어 및 라이브러리
Swift
MVVM
Kingfisher
Alamofire
SwiftSoup
UIKit

요약

LCK 팬들을 위한 LCK 리그 일정, 선수 정보, 순위, 뉴스 제공 앱

기능 소개

금일 경기 일정 및 월별 경기 일정 제공
팀별 선수 정보 제공
리그 순위 제공
리그 시즌 선택 기능
뉴스 제공

앱 스크린샷

개발 환경

iOS: 15.0
Xcode: 13.1
Swift: 5.5
Dependency Manager: CocoaPods
Search
라이브러리 (1)
라이브러리
목적
웹 크롤링
CocoaPods
서버 통신
CocoaPods
COUNT3

구현하면서 어려웠던 점

1.
API 통신을 통해서 월별 일정을 가져올 때 문제가 있었습니다. 프로리그 월별 일정을 가져올 때 요청을 2번 해야하는 상황이었는데, 단순히 DispatchQueue로 각각의 작업을 비동기로 처리하게 되면 2번의 요청 중 먼저 응답 받은 요청을 DispatchQueue가 먼저 데이터 바인딩하는 문제가 발생했습니다.
두 번의 요청으로 모든 데이터를 가져온 다음에 UI가 업데이트 되도록 설계하기 위해서 DispatchGroup을 사용했습니다. DispatchGroup을 이용해서 그룹에 포함된 모든 작업이 완료된 후, 일정을 escaping closure로 데이터 바인딩할 수 있도록 구현했습니다.
구현 코드는 다음과 같습니다.
func getMonthSchedule( urls: [URL?], completion: @escaping (([ReceivedScheduleModel]) -> Void) ) { let dispatchGroup = DispatchGroup() var scheduleList: [ReceivedScheduleModel] = [] urls.forEach { url in dispatchGroup.enter() guard let requestUrl = url else { print(NetworkError.invalidUrl) return } let headers = [ "Accept": "application/json", "Authorization": Storage().apiKey ] let request = NSMutableURLRequest( url: requestUrl, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0 ) request.httpMethod = "GET" request.allHTTPHeaderFields = headers let urlSession = URLSession.shared urlSession.dataTask( with: request as URLRequest, completionHandler: { (data, response, error) -> Void in guard let responseStatus = response as? HTTPURLResponse, responseStatus.statusCode == 200 else { print(NetworkError.invalidResponse) return } guard let data = data, error == nil else { print(NetworkError.invalidData) return } do { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 decoder.keyDecodingStrategy = .convertFromSnakeCase scheduleList += try decoder.decode([ReceivedScheduleModel].self, from: data) } catch { print(NetworkError.decodingError) } dispatchGroup.leave() } ).resume() } dispatchGroup.notify(queue: .global()) { completion(scheduleList) } }
Swift
복사
작업이 시작될 때 DispatchGroup에 들어가고(enter) 작업이 끝나고(leave) 그룹의 모든 작업이 끝나면 notify 메서드로 escaping closure에 스케줄 리스트를 전달합니다.
2.
일정 화면에서 custom tab bar에서 indicator view를 구현할 때 어려움이 있었습니다. 월 탭바를 collection view로 구현했는데, 12개의 탭을 한 화면에 모두 담을 수 없어, 스크롤이 가능하도록 구현해야 했습니다.
페이지 스크롤 또는 특정 월 탭 클릭 시, 인디케이터 이동
월 탭바 스크롤 시, 인디케이터의 x좌표를 현재 클릭된 월 탭의 x좌표와 일치시키기
이를 해결하기 위해서 월 탭바 클릭 이벤트에 대한 delegate를 정의하고, 이벤트에 대한 처리로 scrollToItem() 메서드를 이용해서 페이지를 이동시키고 인디케이터의 left constraint를 선택된 셀의 minX 값과 일치시켰습니다.
코드는 다음과 같습니다.
// CustomMonthBar - UICollectionViewDelegate func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedMonthIndexPath = indexPath // 월 탭바 클릭 시, 클릭된 셀의 위치를 전달 customTabBarDelegate?.customTabBarIndex(scroll: selectedMonthIndexPath.row) } // ScheduleViewController - CustomTabBarDelegate extension ScheduleViewController: CustomTabBarDelegate { func customTabBarIndex(scroll index: Int) { selectedMonthIndexPath = IndexPath(item: index, section: 0) customMonthBar.monthCollectionView.scrollToItem(at: selectedMonthIndexPath, at: .centeredHorizontally, animated: true) pageMonthCollectionView.scrollToItem(at: selectedMonthIndexPath, at: .centeredHorizontally, animated: true) IndicatorCenterXConstraint.constant = CGFloat(selectedMonthIndexPath.row) * (pageMonthCollectionView.frame.width + 12) } func pastScrollOffsetX(offsetX pastScrollOffsetX: CGFloat) { self.pastScrollOffsetX = pastScrollOffsetX } } // ScheduleViewController - UICollectionViewDelegate extension ScheduleViewController: UICollectionViewDelegate { // 페이지 스크롤 또는 월 클릭 시, 인디케이터의 left constraint 조정 func scrollViewDidScroll(_ scrollView: UIScrollView) { let selectedX = 60 * CGFloat(selectedMonthIndexPath.row) - pastScrollOffsetX customMonthBar.indicatorViewLeftConstraint.constant = selectedX } // 페이지 스크롤 시, 페이지를 이동시키고, 월 탭 선택 업데이트 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { pageMonthCollectionView.selectItem(at: selectedMonthIndexPath, animated: true, scrollPosition: .centeredHorizontally) if selectedMonthIndexPath.row < 11 && CGFloat(selectedMonthIndexPath.row) < targetContentOffset.pointee.x / pageMonthCollectionView.frame.width { let nextIndexPath = IndexPath(item: selectedMonthIndexPath.row + 1, section: 0) pageMonthCollectionView.scrollToItem(at: nextIndexPath, at: .centeredHorizontally, animated: true) customMonthBar.monthCollectionView.selectItem( at: nextIndexPath, animated: true, scrollPosition: .centeredHorizontally ) selectedMonthIndexPath = nextIndexPath customMonthBar.selectedMonthIndexPath = selectedMonthIndexPath IndicatorCenterXConstraint.constant = CGFloat(self.selectedMonthIndexPath.row) * (self.pageMonthCollectionView.frame.width + 12) } else if selectedMonthIndexPath.row > 0 && CGFloat(selectedMonthIndexPath.row) > targetContentOffset.pointee.x / pageMonthCollectionView.frame.width { let pastIndexPath = IndexPath(item: selectedMonthIndexPath.row - 1, section: 0) pageMonthCollectionView.scrollToItem(at: pastIndexPath, at: .centeredHorizontally, animated: true) customMonthBar.monthCollectionView.selectItem( at: pastIndexPath, animated: true, scrollPosition: .centeredHorizontally ) selectedMonthIndexPath = pastIndexPath customMonthBar.selectedMonthIndexPath = selectedMonthIndexPath IndicatorCenterXConstraint.constant = CGFloat(self.selectedMonthIndexPath.row) * (self.pageMonthCollectionView.frame.width + 12) } } }
Swift
복사
월 탭바를 스크롤했을 때, 인디케이터의 x좌표를 선택한 월 탭의 x좌표와 일치시키는 코드는 다음과 같습니다.
// CustomMonthBar - UICollectionViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { let selectedCell = monthCollectionView.cellForItem(at: selectedMonthIndexPath) customTabBarDelegate?.pastScrollOffsetX(offsetX: scrollView.contentOffset.x) // 셀이 현재 화면에 나타나있는 경우, 인디케이터 뷰의 left constrint를 선택한 월 탭의 minX 값과 일치시키기 if let selectedCell = selectedCell { indicatorViewLeftConstraint.constant = selectedCell.frame.minX - scrollView.contentOffset.x } }
Swift
복사