-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature [RM88] Handle Server Error #91
Conversation
Displays an error message in the middle of the screen if an error occurs. The user can tap the refresh button to try again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few things here and there but looks good for your first time dealing with errors etc
if let err = decodingError as NSError? { | ||
DispatchQueue.main.async { | ||
error(err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think there is any need to shorten the variable name here or anywhere else like this
if let err = decodingError as NSError? { | |
DispatchQueue.main.async { | |
error(err) | |
} | |
if let error = decodingError as NSError? { | |
DispatchQueue.main.async { | |
error(error) | |
} |
@@ -16,14 +17,16 @@ final class RickAndMortyService: RickAndMortyServiceProtocol { | |||
success(decodedData) | |||
} | |||
} catch let decodingError { | |||
DispatchQueue.main.async { | |||
error(decodingError) | |||
if let err = decodingError as NSError? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a possibility that we are swallowing an error in the case that the error is not an NSError
so we should add an else to catch these with a more generic error response. Something like Error: wtf happened here. We don't know. Probably couldnt parse the error as NSError though
|
||
if let err = error { | ||
if err.code == 4864 { | ||
self.characterListViewState.errorMessage = "Wubba Lubba Dub Dub! There was a problem loading the characters." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
excellent erro message 😂
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..") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be worth extracting this all into a neat little error handling function. Pyramids of doom are appearing, normally a good sign
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah definitely, for simplicity Im just doing a custom message for that code at the moment. All the rest of the errors just display the localized description
if viewModel.characterListViewState.state == .error { | ||
ErrorView(errorMessage: viewModel.characterListViewState.errorMessage, refreshAction: viewModel.loadCharacters) | ||
} else if viewModel.characterListViewState.state == .loading { | ||
ProgressView() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this logic should be in the VM. It shouldnt be possible to unit test the view :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats your suggestion to fix this? I think in swiftUI its normal to use conditionals based on the state in the view
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what happens in most cases now is you would have a reactive chain connected to the state, so whenever that changes the view would update to ErrorView
or ProgressView
. In your case though id have a function in the viewModel called viewForState()
if viewModel.characterListViewState.state == .error { | |
ErrorView(errorMessage: viewModel.characterListViewState.errorMessage, refreshAction: viewModel.loadCharacters) | |
} else if viewModel.characterListViewState.state == .loading { | |
ProgressView() | |
} | |
viewModel.viewForState() |
In the viewModel:
func viewForState() -> View {
if viewModel.characterListViewState.state == .error {
return ErrorView(errorMessage: characterListViewState.errorMessage, refreshAction: loadCharacters)
} else if characterListViewState.state == .loading {
return ProgressView()
}
}
This isnt a perfect solution, as ideally the ViewModel doesnt know anything about views either and this function would exist inside a ViewFactory of some sort that you would pass as a dependency to the VM. That would look something like:
import SwiftUI
protocol ViewFactoryProtocol {
func viewForState(errorMessage: String, refreshAction: () -> Void) -> View
}
class ViewFactory {
static func viewForState(errorMessage: String, refreshAction: () -> Void) -> View {
if viewModel.characterListViewState.state == .error {
return ErrorView(errorMessage: characterListViewState.errorMessage, refreshAction: loadCharacters)
} else if characterListViewState.state == .loading {
return ProgressView()
}
}
}
The factory name `viewForState` could be more descriptive but my morning brain aint working for now.
Hope this is clear! Ping me if not :)
Now the ViewModel would be updated with the init to look like so:
private let viewFactory: ViewFactoryProtocol
init(viewFactory: ViewFactoryProtocol) {
self.viewFactory = viewFactory
}
then your viewForState func would be even cleaner and look like this:
func viewForState() -> View {
return viewFactory.viewForState(errorMessage: characterListViewState.errorMessage, refreshAction: loadCharacters)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wrumble Do you think viewForState()
should be in viewmodel? Wouldn't that make view model less testable? If this would not be swift UI the logic we're talking about would be in ViewControllers which belong to View layer. If we want to extract that we could call the factory from CharacterListView.swift
and test the factory. wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, i guess so, whatever your preference i guess, i just made the step from moving it to view model then to factory didnt think any further 😂
let refreshAction: () -> () | ||
|
||
var body: some View { | ||
if let error = errorMessage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about logic here. You may need a tiny VM for this View
if error.code == 4864 { | ||
self.characterListViewState.errorMessage = "Wubba Lubba Dub Dub! There was a problem loading the characters." | ||
} else { | ||
self.characterListViewState.errorMessage = error.localizedDescription | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally would still extract this to a function called handle(error: NSError)
as its cleaner and gets this loadCharactes function a bit closer to Singular responsibility :)
https://github.com/orgs/novoda/projects/1#card-67126216
Error messages are now displayed to the user if an error occurs. The user can tap the refresh button in the message to try again.