Complete Challenge 3

> Our app silently fails when errors occur during biometric authentication. Add code to show those errors in an alert. But be careful: you can only add one alert() modifier to each view.
This commit is contained in:
CypherPoet 2019-12-18 22:22:01 -06:00
parent 53dc10ab4a
commit 74ac5d349b
4 changed files with 43 additions and 25 deletions

View File

@ -14,7 +14,7 @@ import LocalAuthentication
struct AppState { struct AppState {
var authenticationService: AuthenticatingService var authenticationService: AuthenticationService
var locationCollectionsState: LocationCollectionsState var locationCollectionsState: LocationCollectionsState
var wikiPagesState: WikiPagesState var wikiPagesState: WikiPagesState
@ -24,7 +24,7 @@ struct AppState {
init( init(
authenticationService: AuthenticatingService = AuthenticationService(laContextType: LAContext.self), authenticationService: AuthenticationService = AuthenticationService(laContextType: LAContext.self),
locationCollectionsState: LocationCollectionsState = .init(), locationCollectionsState: LocationCollectionsState = .init(),
wikiPagesState: WikiPagesState = .init() wikiPagesState: WikiPagesState = .init()
) { ) {

View File

@ -24,19 +24,12 @@ extension LAContext: LAContextType {}
protocol AuthenticatingService { class AuthenticationService {
/// - Parameter reason: The app-provided reason for requesting authentication,
/// which displays in the authentication dialog presented to the user.
func authenticate(reason: String) -> AnyPublisher<Void, Error>
}
final class AuthenticationService: AuthenticatingService {
static let authReason = "Please authenticate to unlock this app and access saved locations." static let authReason = "Please authenticate to unlock this app and access saved locations."
enum Error: Swift.Error { enum Error: Swift.Error, Identifiable {
var id: String { self.localizedDescription }
case noBiometricsEnabled(Swift.Error?) case noBiometricsEnabled(Swift.Error?)
case evaluationFailed(Swift.Error?) case evaluationFailed(Swift.Error?)
} }
@ -46,12 +39,14 @@ final class AuthenticationService: AuthenticatingService {
private var context: LAContextType? private var context: LAContextType?
init(laContextType: LAContextType.Type) { init(laContextType: LAContextType.Type = LAContext.self) {
self.laContextType = laContextType self.laContextType = laContextType
} }
func authenticate(reason: String) -> AnyPublisher<Void, Swift.Error> { /// - Parameter reason: The app-provided reason for requesting authentication,
/// which displays in the authentication dialog presented to the user.
func authenticate(reason: String) -> AnyPublisher<Void, Error> {
let context = laContextType.init() let context = laContextType.init()
self.context = context self.context = context
@ -94,8 +89,8 @@ final class AuthenticationService: AuthenticatingService {
extension SampleData { extension SampleData {
class AuthService: AuthenticatingService { class AuthService: AuthenticationService {
func authenticate(reason: String) -> AnyPublisher<Void, Error> { override func authenticate(reason: String) -> AnyPublisher<Void, Error> {
Empty().eraseToAnyPublisher() Empty().eraseToAnyPublisher()
} }
} }

View File

@ -30,6 +30,7 @@ extension LocationCollectionsContainerView {
} }
} }
.navigationBarTitle("PlaceCase") .navigationBarTitle("PlaceCase")
.alert(item: $viewModel.authenticationError) { _ in self.authenticationErrorAlert }
} }
} }
@ -70,6 +71,15 @@ extension LocationCollectionsContainerView {
} }
.padding() .padding()
} }
private var authenticationErrorAlert: Alert {
.init(
title: Text(viewModel.authenticationErrorAlertTitle),
message: Text(viewModel.authenticationErrorAlertBody),
dismissButton: .default(Text("OK"))
)
}
} }

View File

@ -14,20 +14,19 @@ import Combine
final class LocationCollectionsContainerViewModel: ObservableObject { final class LocationCollectionsContainerViewModel: ObservableObject {
private var subscriptions = Set<AnyCancellable>() private var subscriptions = Set<AnyCancellable>()
private let authService: AuthenticatingService private let authService: AuthenticationService
// MARK: - Published Properties // MARK: - Published Properties
@Published var isAuthenticated: Bool = false @Published var isAuthenticated: Bool = false
@Published var authenticationError: AuthenticationService.Error? = nil
// MARK: - Init // MARK: - Init
init( init(
authService: AuthenticatingService authService: AuthenticationService
) { ) {
self.authService = authService self.authService = authService
setupSubscribers()
} }
} }
@ -49,7 +48,7 @@ extension LocationCollectionsContainerViewModel {
switch completion { switch completion {
case .failure(let error): case .failure(let error):
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
print("Authentication failed: \(error)") self?.authenticationError = error
self?.isAuthenticated = false self?.isAuthenticated = false
} }
default: default:
@ -67,11 +66,25 @@ extension LocationCollectionsContainerViewModel {
} }
} }
// MARK: - Computed
extension LocationCollectionsContainerViewModel {
var authenticationErrorAlertTitle: String { "Authentication Failed" }
var authenticationErrorAlertBody: String {
guard let error = authenticationError else { return "" }
switch error {
case .noBiometricsEnabled:
return "No Biomentric authentication is enabled on this device."
case .evaluationFailed(_):
return "An error occured while attempting evaluation."
}
}
}
// MARK: - Private Helpers // MARK: - Private Helpers
private extension LocationCollectionsContainerViewModel { private extension LocationCollectionsContainerViewModel {
func setupSubscribers() {
}
} }