From cc5ffd8b58cab11f262dedb1638a1a0488650e3c Mon Sep 17 00:00:00 2001 From: swg99 <87419041+swg99@users.noreply.github.com> Date: Tue, 24 Aug 2021 14:32:37 +0100 Subject: [PATCH 1/2] Error handling and progress view Displays an error message in the middle of the screen if an error occurs. The user can tap the refresh button to try again. --- Rick-and-Morty/CharacterRepository.swift | 9 ++--- .../Rick And Morty.xcodeproj/project.pbxproj | 4 ++ .../Services/RickAndMortyService.swift | 13 ++++--- .../RickAndMortyServiceProtocol.swift | 2 +- .../ViewModels/CharacterListViewModel.swift | 29 ++++++++++++++- .../Views/CharacterListView.swift | 19 +++++++--- .../Rick And Morty/Views/ErrorView.swift | 37 +++++++++++++++++++ 7 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 Rick-and-Morty/Rick And Morty/Views/ErrorView.swift diff --git a/Rick-and-Morty/CharacterRepository.swift b/Rick-and-Morty/CharacterRepository.swift index 8ce7431..d71f292 100644 --- a/Rick-and-Morty/CharacterRepository.swift +++ b/Rick-and-Morty/CharacterRepository.swift @@ -1,7 +1,7 @@ import Foundation protocol CharacterRepositoryProtocol { - func getCharacters(completion: @escaping (([Character]) -> Void)) + func getCharacters(completion: @escaping (([Character]) -> Void), error: @escaping ((NSError?) -> Void)) } final class CharacterRepository: CharacterRepositoryProtocol { @@ -9,7 +9,7 @@ final class CharacterRepository: CharacterRepositoryProtocol { private let rickAndMortyService: RickAndMortyServiceProtocol = RickAndMortyService() - func getCharacters(completion: @escaping (([Character]) -> Void)) { + func getCharacters(completion: @escaping (([Character]) -> Void), error: @escaping ((NSError?) -> Void)) { if let url = characterPageURL { rickAndMortyService.fetchData(url: url) { (charactersResponse: CharacterResponse) in if let nextURLString = charactersResponse.info.next { @@ -21,9 +21,8 @@ final class CharacterRepository: CharacterRepositoryProtocol { } completion(charactersResponse.characters) - } error: { error in - print(error.debugDescription) - return + } error: { (err: NSError?) in + error(err) } } } diff --git a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj index c33b2ac..58fc4e1 100644 --- a/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj +++ b/Rick-and-Morty/Rick And Morty.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1711B39E26B1898100BE935B /* CharacterListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1711B39D26B1898100BE935B /* CharacterListView.swift */; }; 174655E826CBB74100A52819 /* EpisodeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174655E726CBB74100A52819 /* EpisodeDetailView.swift */; }; 174655EA26CBBD2300A52819 /* EpisodeDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174655E926CBBD2300A52819 /* EpisodeDetailViewModel.swift */; }; + 174878F626D4E3CA004293AB /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174878F526D4E3CA004293AB /* ErrorView.swift */; }; 174B064B26C6611D0080ADD0 /* RickAndMortyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B064A26C6611D0080ADD0 /* RickAndMortyService.swift */; }; 174B064D26C661470080ADD0 /* RickAndMortyServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B064C26C661470080ADD0 /* RickAndMortyServiceProtocol.swift */; }; 174B065126C671580080ADD0 /* CharacterRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174B065026C671580080ADD0 /* CharacterRepository.swift */; }; @@ -41,6 +42,7 @@ 1711B39D26B1898100BE935B /* CharacterListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterListView.swift; sourceTree = ""; }; 174655E726CBB74100A52819 /* EpisodeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeDetailView.swift; sourceTree = ""; }; 174655E926CBBD2300A52819 /* EpisodeDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeDetailViewModel.swift; sourceTree = ""; }; + 174878F526D4E3CA004293AB /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 174B064A26C6611D0080ADD0 /* RickAndMortyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickAndMortyService.swift; sourceTree = ""; }; 174B064C26C661470080ADD0 /* RickAndMortyServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RickAndMortyServiceProtocol.swift; sourceTree = ""; }; 174B065026C671580080ADD0 /* CharacterRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterRepository.swift; sourceTree = ""; }; @@ -140,6 +142,7 @@ 1711B39D26B1898100BE935B /* CharacterListView.swift */, 17588BAE26C273BB008ECC31 /* CharacterCard.swift */, 174655E726CBB74100A52819 /* EpisodeDetailView.swift */, + 174878F526D4E3CA004293AB /* ErrorView.swift */, 174941D126CD1216001AFD9A /* Utilities */, ); path = Views; @@ -308,6 +311,7 @@ B811686D1CFF1C9900301A0A /* AppDelegate.swift in Sources */, 174B065926C680850080ADD0 /* EpisodeRepository.swift in Sources */, 174655EA26CBBD2300A52819 /* EpisodeDetailViewModel.swift in Sources */, + 174878F626D4E3CA004293AB /* ErrorView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift index 1cc4f0d..c4bcff9 100644 --- a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift +++ b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift @@ -3,7 +3,8 @@ import Foundation final class RickAndMortyService: RickAndMortyServiceProtocol { static let baseURL = URL(string: "https://rickandmortyapi.com/api")! - func fetchData(url: URL, success: @escaping (T) -> (), error: @escaping (Error?) -> ()) { + func fetchData(url: URL, success: @escaping (T) -> (), error: @escaping (NSError?) -> ()) { + let request = URLRequest(url: url) URLSession.shared.dataTask(with: request) { data, response, requestError in @@ -16,14 +17,16 @@ final class RickAndMortyService: RickAndMortyServiceProtocol { success(decodedData) } } catch let decodingError { - DispatchQueue.main.async { - error(decodingError) + if let err = decodingError as NSError? { + DispatchQueue.main.async { + error(err) + } } } } else { - if requestError != nil { + if let err = requestError as NSError? { DispatchQueue.main.async { - error(requestError) + error(err) } } } diff --git a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift index f91a2d2..6a0fb27 100644 --- a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift +++ b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift @@ -1,5 +1,5 @@ import Foundation protocol RickAndMortyServiceProtocol { - func fetchData(url: URL, success: @escaping (T) -> (), error: @escaping (Error?) -> ()) + func fetchData(url: URL, success: @escaping (T) -> (), error: @escaping (NSError?) -> ()) } diff --git a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift index b517e16..7657728 100644 --- a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift +++ b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift @@ -5,7 +5,15 @@ final class CharacterListViewModel: ObservableObject { struct CharacterListViewState { let title: String = "Characters" + var errorMessage: String? = nil var characters: [Character] + var state: State = .doneLoading + + enum State { + case loading + case doneLoading + case error + } } private let characterRepository: CharacterRepositoryProtocol = CharacterRepository() @@ -16,11 +24,28 @@ final class CharacterListViewModel: ObservableObject { } func loadCharacters() { - characterRepository.getCharacters { - characters in + characterListViewState.state = .loading + + characterRepository.getCharacters { characters in + self.characterListViewState.state = .doneLoading + self.characterListViewState.errorMessage = nil + for character in characters { self.characterListViewState.characters.append(character) } + } error: { error in + + self.characterListViewState.state = .error + + if let err = error { + if err.code == 4864 { + self.characterListViewState.errorMessage = "Wubba Lubba Dub Dub! There was a problem loading the characters." + } else { + self.characterListViewState.errorMessage = err.localizedDescription + } + + self.characterListViewState.errorMessage?.append(" Tap the refresh button to try again..") + } } } diff --git a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift index 69636aa..e54597a 100644 --- a/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift +++ b/Rick-and-Morty/Rick And Morty/Views/CharacterListView.swift @@ -5,12 +5,21 @@ struct CharacterListView: View { var body: some View { NavigationView { - List(viewModel.characterListViewState.characters, id: \.id) { character in - CharacterCard(viewModel: CharacterCardViewModel(character: character)) - .onAppear(perform: { - viewModel.loadIfNeeded(characterID: character.id) - }) + ZStack { + List(viewModel.characterListViewState.characters, id: \.id) { character in + CharacterCard(viewModel: CharacterCardViewModel(character: character)) + .onAppear(perform: { + viewModel.loadIfNeeded(characterID: character.id) + }) + } + + if viewModel.characterListViewState.state == .error { + ErrorView(errorMessage: viewModel.characterListViewState.errorMessage, refreshAction: viewModel.loadCharacters) + } else if viewModel.characterListViewState.state == .loading { + ProgressView() + } } + .listStyle(PlainListStyle()) .navigationTitle(viewModel.characterListViewState.title) } } diff --git a/Rick-and-Morty/Rick And Morty/Views/ErrorView.swift b/Rick-and-Morty/Rick And Morty/Views/ErrorView.swift new file mode 100644 index 0000000..f2b6781 --- /dev/null +++ b/Rick-and-Morty/Rick And Morty/Views/ErrorView.swift @@ -0,0 +1,37 @@ +// +// ErrorView.swift +// Rick And Morty +// +// Created by Scottie Gray on 2021-08-24. +// Copyright © 2021 Novoda. All rights reserved. +// + +import SwiftUI + +struct ErrorView: View { + let errorMessage: String? + let refreshAction: () -> () + + var body: some View { + if let error = errorMessage { + VStack { + Text(error) + .foregroundColor(.gray) + .multilineTextAlignment(.center) + .padding() + Button(action: refreshAction, label: { + Image(systemName: "arrow.clockwise") + .resizable() + .frame(width: 30, height: 30, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) + }) + } + } + } +} + +struct ErrorView_Previews: PreviewProvider { + static var previews: some View { + ErrorView(errorMessage: "Wubba Lubba Dub Dub! There was a problem loading. Tap the button to try again.", refreshAction: {}) + + } +} From b99cc7516353f817d0ec5b5c7538d27da94a91b9 Mon Sep 17 00:00:00 2001 From: swg99 <87419041+swg99@users.noreply.github.com> Date: Thu, 26 Aug 2021 10:58:25 +0100 Subject: [PATCH 2/2] Adressed comments --- Rick-and-Morty/CharacterRepository.swift | 8 ++++---- Rick-and-Morty/EpisodeRepository.swift | 2 +- .../Services/RickAndMortyService.swift | 14 +++++++++----- .../Services/RickAndMortyServiceProtocol.swift | 2 +- .../ViewModels/CharacterListViewModel.swift | 8 ++++---- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Rick-and-Morty/CharacterRepository.swift b/Rick-and-Morty/CharacterRepository.swift index d71f292..22e645f 100644 --- a/Rick-and-Morty/CharacterRepository.swift +++ b/Rick-and-Morty/CharacterRepository.swift @@ -1,7 +1,7 @@ import Foundation protocol CharacterRepositoryProtocol { - func getCharacters(completion: @escaping (([Character]) -> Void), error: @escaping ((NSError?) -> Void)) + func getCharacters(completion: @escaping (([Character]) -> Void), fail: @escaping ((NSError?) -> Void)) } final class CharacterRepository: CharacterRepositoryProtocol { @@ -9,7 +9,7 @@ final class CharacterRepository: CharacterRepositoryProtocol { private let rickAndMortyService: RickAndMortyServiceProtocol = RickAndMortyService() - func getCharacters(completion: @escaping (([Character]) -> Void), error: @escaping ((NSError?) -> Void)) { + func getCharacters(completion: @escaping (([Character]) -> Void), fail: @escaping ((NSError?) -> Void)) { if let url = characterPageURL { rickAndMortyService.fetchData(url: url) { (charactersResponse: CharacterResponse) in if let nextURLString = charactersResponse.info.next { @@ -21,8 +21,8 @@ final class CharacterRepository: CharacterRepositoryProtocol { } completion(charactersResponse.characters) - } error: { (err: NSError?) in - error(err) + } fail: { (error: NSError?) in + fail(error) } } } diff --git a/Rick-and-Morty/EpisodeRepository.swift b/Rick-and-Morty/EpisodeRepository.swift index a130d30..06dea54 100644 --- a/Rick-and-Morty/EpisodeRepository.swift +++ b/Rick-and-Morty/EpisodeRepository.swift @@ -16,7 +16,7 @@ final class EpisodeRepository: EpisodeRepositoryProtocol { rickAndMortyService.fetchData(url: url) { (episode: Episode) in EpisodeRepository.cachedEpisodeNames[urlString] = episode completion(episode) - } error: { error in + } fail: { error in print(error.debugDescription) return } diff --git a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift index c4bcff9..9f76088 100644 --- a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift +++ b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyService.swift @@ -3,7 +3,7 @@ import Foundation final class RickAndMortyService: RickAndMortyServiceProtocol { static let baseURL = URL(string: "https://rickandmortyapi.com/api")! - func fetchData(url: URL, success: @escaping (T) -> (), error: @escaping (NSError?) -> ()) { + func fetchData(url: URL, success: @escaping (T) -> (), fail: @escaping (NSError?) -> ()) { let request = URLRequest(url: url) @@ -17,17 +17,21 @@ final class RickAndMortyService: RickAndMortyServiceProtocol { success(decodedData) } } catch let decodingError { - if let err = decodingError as NSError? { + if let error = decodingError as NSError? { DispatchQueue.main.async { - error(err) + fail(error) } + } else { + print("Unknown Error: \(decodingError.localizedDescription)") } } } else { - if let err = requestError as NSError? { + if let error = requestError as NSError? { DispatchQueue.main.async { - error(err) + fail(error) } + } else { + print("Unknown Error: \(String(describing: requestError?.localizedDescription))") } } }.resume() diff --git a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift index 6a0fb27..b5d8086 100644 --- a/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift +++ b/Rick-and-Morty/Rick And Morty/Services/RickAndMortyServiceProtocol.swift @@ -1,5 +1,5 @@ import Foundation protocol RickAndMortyServiceProtocol { - func fetchData(url: URL, success: @escaping (T) -> (), error: @escaping (NSError?) -> ()) + func fetchData(url: URL, success: @escaping (T) -> (), fail: @escaping (NSError?) -> ()) } diff --git a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift index 7657728..646bbc9 100644 --- a/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift +++ b/Rick-and-Morty/Rick And Morty/ViewModels/CharacterListViewModel.swift @@ -33,15 +33,15 @@ final class CharacterListViewModel: ObservableObject { for character in characters { self.characterListViewState.characters.append(character) } - } error: { error in + } fail: { error in self.characterListViewState.state = .error - if let err = error { - if err.code == 4864 { + if let error = error { + if error.code == 4864 { self.characterListViewState.errorMessage = "Wubba Lubba Dub Dub! There was a problem loading the characters." } else { - self.characterListViewState.errorMessage = err.localizedDescription + self.characterListViewState.errorMessage = error.localizedDescription } self.characterListViewState.errorMessage?.append(" Tap the refresh button to try again..")