Reputation: 1
Could you explain how and why it acts like this and if you have some exact sources also please share.
I am following Youtube tutorial where he used Rick and Morty API, so I want to show Episode list and in each cell I want to display episode name and character list (this one I have to fetch from the list of character URLs). SO I am having trouble with fetching the character list
If I use DispatchGroup, I can easily fetch my initial episodes with characters of each episode, but when scroll down and trigger additional fetch it stops working.
protocol RMEpisodeViewDelegate: AnyObject {
func initialEpisodesFetched()
func didLoadAdditionalCharacters(with newIndexpath: [IndexPath])
}
class RMEpisodeViewModel: NSObject {
private var episodeViewModels: [RMEpisodeViewCellViewModel] = []
private var info: RMInfo?
private var loadingMore = false
weak var delegate: RMEpisodeViewDelegate?
func fetchEpisodes(episodesURL: URL? = nil) {
var rmRequest: RMRequest = .listEpisodesRequests
if let episodesURL = episodesURL {
guard let request = RMRequest(url: episodesURL) else { return }
rmRequest = request
}
RMService.shared.execute(
rmRequest,
expecting: RMResponse<RMEpisode>.self)
{ [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
self.info = response.info
self.setupEpisodeViewModel(episodes: response.results) {
DispatchQueue.main.async {
if let episodesURL = episodesURL {
let indexPath = (self.episodeViewModels.count - response.results.count ..< self.episodeViewModels.count).map { IndexPath(row: $0, section: 0) }
self.delegate?.didLoadAdditionalCharacters(with: indexPath)
self.loadingMore = false
} else {
self.delegate?.initialEpisodesFetched()
}
self.loadingMore = false
}
}
case .failure(let error):
print(error)
self.loadingMore = false
}
}
}
private func setupEpisodeViewModel(episodes: [RMEpisode], completion: @escaping () -> Void) {
let dispatchGroup = DispatchGroup()
for episode in episodes {
guard let characterURLs = episode.characters else { continue }
dispatchGroup.enter()
fetchEpisodeCharacters(characterURLs: characterURLs) { characters in
self.episodeViewModels.append(RMEpisodeViewCellViewModel(episode: episode, characters: characters))
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
completion()
}
}
private func fetchEpisodeCharacters(characterURLs: [String], completion: @escaping ([RMCharacter]) -> Void) {
var characters: [RMCharacter] = []
let dispatchGroup = DispatchGroup()
for urlString in characterURLs {
guard let url = URL(string: urlString) else { continue }
dispatchGroup.enter()
fetchCharacter(url: url) { result in
switch result {
case .success(let character):
characters.append(character)
case .failure(let error):
print(error)
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
completion(characters)
}
}
private func fetchCharacter(url: URL, completion: @escaping (Result<RMCharacter, Error>) -> Void) {
guard let rmRequest = RMRequest(url: url) else {
completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
return
}
RMService.shared.execute(rmRequest, expecting: RMCharacter.self, completion: completion)
}
}
extension RMEpisodeViewModel: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
episodeViewModels.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RMEpisodeViewCell.identifier, for: indexPath) as? RMEpisodeViewCell else {
fatalError("Cell type mismatch")
}
cell.configure(viewModel: episodeViewModels[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let bounds = UIScreen.main.bounds
return CGSize(width: bounds.width - 24, height: 0.2 * bounds.height)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: RMFooterLoadingCollectionReusableView.identifier, for: indexPath) as? RMFooterLoadingCollectionReusableView else {
fatalError("Footer type mismatch")
}
info?.next != nil ? footer.startAnimating() : footer.stopAnimating()
return footer
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: info?.next != nil ? 100 : 0)
}
}
extension RMEpisodeViewModel: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
let totalLength = (episodeViewModels.count - 1) * 150
if Int(offset) - totalLength > 0, !loadingMore, info?.next != nil {
loadingMore = true
guard let url = URL(string: info!.next!) else {
loadingMore = false
return
}
fetchEpisodes(episodesURL: url)
}
}
}
Screen shot of the implementation with DispatchGroup
Thread 8 Queue : com.apple.CFNetwork.CacheDB-write (serial)
Thread 9 Queue : com.apple.NSURLSession-delegate (serial)
Thread 10 Queue : com.apple.NSURLSession-work (serial)
and now This code do not use DispatchGroup
final class RMEpisodeViewModel: NSObject {
weak var delegate: RMEpisodeViewDelegate?
var info: RMInfo? = nil
var episodeViewModels: [RMEpisodeViewCellViewModel] = []
var loadingMore = false
func fetchEpisodes(episodesURL: URL? = nil) {
var rmRequest: RMRequest = .listEpisodesRequests
if let episodesURL = episodesURL {
guard let request = RMRequest(url: episodesURL) else { return }
rmRequest = request
}
RMService.shared.execute(rmRequest, expecting: RMResponse<RMEpisode>.self, completion: {
[weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
self.info = response.info
self.setupEpisodeViewModel(episodes: response.results) {
DispatchQueue.main.async {
if let episodesURL = episodesURL {
let indexPath = (self.episodeViewModels.count - response.results.count ..< self.episodeViewModels.count).map { IndexPath(row: $0, section: 0)}
self.delegate?.didLoadAdditionalCharacters(
with: indexPath)
} else {
self.delegate?.initialEpisodesFetched()
}
self.loadingMore = false
}
}
case .failure(let error):
print(error)
}
})
}
func setupEpisodeViewModel(episodes: [RMEpisode], completion: @escaping () -> Void) {
var indexPaths = [IndexPath]()
let startCount = self.episodeViewModels.count
episodes.forEach { episode in
guard let characterURLs = episode.characters else { return }
let characters = fetchEpisodeCharacters(characterURLs: characterURLs)
episodeViewModels.append(RMEpisodeViewCellViewModel(episode: episode, characters: characters))
let indexPath = IndexPath(row: startCount + indexPaths.count, section: 0)
indexPaths.append(indexPath)
}
completion()
}
private func fetchEpisodeCharacters(characterURLs: [String]) -> [RMCharacter] {
var characters: [RMCharacter] = []
characterURLs.forEach { url in
guard let url = URL(string: url) else { return }
fetchCharacter(url: url) { result in
switch result {
case .success(let character):
characters.append(character)
case .failure(let error):
print(error)
}
}
}
return characters
}
private func fetchCharacter(url: URL, completion: @escaping (Result<RMCharacter, Error>) -> Void) {
guard let rmRequest = RMRequest(url: url) else { return }
RMService.shared.execute(rmRequest, expecting: RMCharacter.self, completion: completion)
}
}
I have used both of the ways and the results are totally different is there any way to do the fetching work in both ways
Upvotes: 0
Views: 77