GitHub 사용자의 팔로워를 검색해 볼 수 있습니다. 검색한 팔로워의 GitHub 프로필과 검색한 팔로워를 팔로워 한 목록을 다시 검색할 수 있습니다. 즐겨찾기에 원하는 사용자를 저장할 수 있습니다.
- 주요 개념:
Reusable View
,Pagination
,Diffable Data Source
사용자명 확인 | 팔로워 검색 |
---|---|
즐겨찾기 추가 | 팔로워 리스트 검색 |
---|---|
프로필 확인 | 다음 팔로워로 이동 |
---|---|
즐겨찾기 삭제 | 라이트/다크 모드 지원 |
---|---|
🔒 문제점
- 매번
View
를 생성하는 경우View Controller
가 복잡해질 수 있다는 문제가 있습니다.
🔑 해결방법
- 이를 해결하기 위해 유사한
View
를 생성할 수 있는reuseable
한View
를 정의하였습니다. 프로젝트 이름의 앞 글자를 따서GFButton
과 같이 정의한View
앞에GF
라는 수식어를 붙였습니다.
🔒 문제점
-
viewDidLoad
메서드에서 다음과 같이navigation bar
에 대한 처리를 진행했습니다.override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground navigationController?.isNavigationBarHidden = false navigationController?.navigationBar.prefersLargeTitles = true }
다음 화면으로 넘어가게 되면 이때부터
navigation bar
를 보이게 하기 위해isNavigationBarHidden
프로퍼티를true
로 변경합니다.위와 같이 코드를 작성한 경우 다음 화면에서 이전 화면으로 돌아갈 때
viewDidLoad
는 다시 호출되지 않아navigation bar
가 보이는 문제가 있었습니다.
🔑 해결방법
-
View lifecycle을 확인하고 매번 다시 호출되는 viewWillAppear 메서드를 활용하여 다음과 같이 코드를 수정하였습니다.
override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground navigationController?.isNavigationBarHidden = false navigationController?.navigationBar.prefersLargeTitles = true } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(false, animated: true) }
위와 같이 코드를 수정한 경우 이전 화면으로 돌아갈 때 viewWillAppear가 다시 호출되며 navigation bar를 감추게 됩니다.
🔒 문제점
-
GitHub에서 계정을 생성 시 Username은 다음과 같은 제약 조건을 따라야 한다.
기존 사용하던 빈 문자열 체크와 같은 처리에서는 위와 같은 조건에 대해 Username이 올바른지 확인하기 어려운 문제가 있었습니다.
🔑 해결방법
-
정규표현식을 활용해 다음과 같은 식을 사용하였습니다.
"/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i"
🔒 문제점
- 화면에 사용자를 위한 알림창을 띄울 때 알림창에 대한 집중도를 높이기 위한 방법이 필요했습니다.
🔑 해결방법
-
이를 해결하기 위해 알림창 뒤에 있는 화면을 블러 처리하거나 투명도를 낮춤으로 알림창에 사용자가 더 집중할 수 있도록 해결하였습니다.
1. 투명도 조절 2. 블러 처리 -
- 배경화면 투명도 조절
view.backgroundColor = UIColor.black.withAlphaComponent(0.75)
-
- 배경화면 블러 처리
let blurEffect = UIBlurEffect(style: .dark) let blurredEffectView = UIVisualEffectView(effect: blurEffect) blurredEffectView.frame = view.bounds view.addSubview(blurredEffectView)
-
🔒 문제점
-
사용자의 팔로워를 받아오기 위해 GitHub에서 제공하는 API를 사용하였습니다.
GitHub Docs: List followers of a user
위 GitHub에서 제공하는 API 문서에 따르면 한 번에 받아 올 수 있는 팔로워의 정보는 최대 100명이었습니다. 사용자의 팔로워가 100명이 넘어가는 경우에 대해 추가적으로 정보를 받아와야 하는 문제가 발생하였습니다.
🔑 해결방법
-
이를 해결하기 위해 사용자의 팔로워를 받아올 때 받은 데이터의 수를 확인하는 코드를 추가하였습니다. 만약 100명의 데이터를 받아오는 경우 이후 추가적인 팔로워가 있을 수 있음을 나타내며 100보다 적은 데이터의 팔로워를 받은 경우 마지막 page의 데이터를 받았다는 것을 나타내도록 하였습니다.
private func getFollowers(username: String, page: Int) { NetworkManager.shared.getFollowers(for: username, page: page) { [weak self] result in guard let self = self else { return } switch result { case .success(let followers): if followers.count < 100 { self.hasMoreFollowers = false } self.followers.append(contentsOf: followers) self.updateData() case .failure(let error): self.presentGFAlertOnMainThread(title: "Bad Stuff Happened", message: error.rawValue, buttonTitle: "Ok") } } }
또한 추가적으로 더 많은 팔로워를 다시 받아오기 위해 언제 데이터를 새로 받아야 하는지를 나타내야 했습니다. 새로 받아올 팔로워가 더 있으며 컬렉션뷰의 맨 아래까지 스크롤을 내린 경우 다음 페이지에서 추가적인 팔로워를 받을 수 있도록 하였습니다.
extension FollowerListViewController: UICollectionViewDelegate { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { let offsetY = scrollView.contentOffset.y let contentHeight = scrollView.contentSize.height let height = scrollView.frame.size.height if offsetY > contentHeight - height { guard hasMoreFollowers else { return } page += 1 getFollowers(username: username, page: page) } } }
🔒 문제점
- 팔로워 목록에서 다른 사용자를 검색하여 새로운 팔로워 목록을 받아올 때 갱신된 목록 화면이 최상단으로 돌아가지 않는 문제가 있었습니다.
🔑 해결방법
-
이를 해결하기 위해
extension FollowerListViewController: UserInfoViewControllerDelegate { func didRequestFollowers(for username: String) { self.username = username title = username page = 1 hasMoreFollowers = true followers.removeAll() filteredFollowers.removeAll() collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .top, animated: true) if isSearching { navigationItem.searchController?.searchBar.text = "" navigationItem.searchController?.isActive = false navigationItem.searchController?.dismiss(animated: false) isSearching = false } getFollowers(username: username, page: page) } }
위와 같이 코드를 작성하였습니다. 특히
scrollToItem()
메서드를 사용하여 0번째 아이템 즉, 컬렉션뷰에 있는 아이템 중 가장 앞에 있는 아이템에 맞춰 스크롤을 끌어올릴 수 있도록 수정하였습니다.
🔒 문제점
- 검색한 팔로워 리스트의 스크롤을 끝까지 내리면
Github API
를 사용하여 추가적인 팔로워를 받아옵니다. 하지만 네트워크를 통해 데이터를 받아오는 중에 사용자는 여러 번 팔로워를 새로 불러오도록 스크롤을 내릴 수 있습니다. 이런 경우 아직 받아오지 않은 데이터가 있음에도 새로운 네트워크 통신이 발생하여 올바르지 않은 동작을 일으킵니다.
🔑 해결방법
- 이를 해결하기 위해
isLoadingMoreFollowers
변수를 생성하여 현재 받아오는 데이터가 있다면 추가적으로 받아오는 작업을 진행하지 못하도록 막았습니다. 먼저 실행한 네트워크 작업이 완료된 후에isLoadingMoreFollowers
는false
로 변경되고 다시 스크롤을 내려 다음 팔로워 리스트를 받아올 수 있습니다.
🔒 문제점
-
getFollowers
메서드를 사용해 팔로워 리스트를 받아옵니다.func getFollowers(for username: String, page: Int, completed: @escaping (Result<[Follower], NetworkError>) -> Void) { let endpoint = baseURL + "\(username)/followers?per_page=100&page=\(page)" guard let url = URL(string: endpoint) else { completed(.failure(.invalidUsername)) return } let task = URLSession.shared.dataTask(with: url) { data, response, error in if let _ = error { completed(.failure(.unableToComplete)) return } guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { completed(.failure(.invalidResponse)) return } guard let data = data else { completed(.failure(.invalidData)) return } do { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let followers = try decoder.decode([Follower].self, from: data) completed(.success(followers)) } catch { completed(.failure(.invalidData)) } } task.resume() }
이렇게 작성한 코드는 네트워크를 사용하여 데이터를 받아옵니다. 코드가 작성한 순서대로 동작하지 않는다는 부분에서 가독성이 좋지 않았습니다.
🔑 해결방법
-
이를 해결하기 위해
async/await
을 사용하여 다음과 같이 코드를 작성해 보았습니다. 작성한 코드를 보면 작성한 순서대로 코드가 동작함을 알 수 있습니다. 이를 통해 코드의 가독성을 높일 수 있었습니다.func getFollowers(for username: String, page: Int) async throws -> [Follower] { let endpoint = baseURL + "\(username)/followers?per_page=100&page=\(page)" guard let url = URL(string: endpoint) else { throw NetworkError.invalidUsername } let (data, response) = try await URLSession.shared.data(from: url) guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } do { return try decoder.decode([Follower].self, from: data) } catch { throw NetworkError.invalidData } }
- 🍎Apple Docs: UICollectionView
- 🍎Apple Docs: UICollectionViewDiffableDataSource
- 🍎Apple Docs: Updating an App to Use Swift Concurrency
- 👨💻Github Docs: 팔로워 API
- 📘blog: Child View Controllers