Skip to content
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

Merged
merged 2 commits into from
Aug 27, 2021

Conversation

swg99
Copy link
Contributor

@swg99 swg99 commented Aug 24, 2021

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.

Displays an error message in the middle of the screen if an error occurs. The user can tap the refresh button to try again.
@swg99 swg99 requested a review from MiWierzbinski August 24, 2021 13:35
@swg99 swg99 linked an issue Aug 24, 2021 that may be closed by this pull request
Copy link
Contributor

@wrumble wrumble left a 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

Comment on lines 20 to 23
if let err = decodingError as NSError? {
DispatchQueue.main.async {
error(err)
}
Copy link
Contributor

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

Suggested change
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? {
Copy link
Contributor

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."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excellent erro message 😂

Comment on lines 38 to 48
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..")
}
Copy link
Contributor

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

Copy link
Contributor Author

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

Comment on lines +16 to +20
if viewModel.characterListViewState.state == .error {
ErrorView(errorMessage: viewModel.characterListViewState.errorMessage, refreshAction: viewModel.loadCharacters)
} else if viewModel.characterListViewState.state == .loading {
ProgressView()
}
Copy link
Contributor

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 :)

Copy link
Contributor Author

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

Copy link
Contributor

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()

Suggested change
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)
}

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?

Copy link
Contributor

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 {
Copy link
Contributor

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

Comment on lines +41 to +45
if error.code == 4864 {
self.characterListViewState.errorMessage = "Wubba Lubba Dub Dub! There was a problem loading the characters."
} else {
self.characterListViewState.errorMessage = error.localizedDescription
}
Copy link
Contributor

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 :)

@swg99 swg99 merged commit 2564e7d into scottie-main Aug 27, 2021
@swg99 swg99 deleted the feature-rm-88-handle-server-error branch August 27, 2021 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Handle Server Error
3 participants