Merge pull request #19 from rock-n-code/improvements/progress-indicator-login

Improvement: Progress indicator in the Login view
This commit is contained in:
Javier Cicchelli 2022-12-18 16:13:41 +01:00 committed by GitHub
commit ca5072e660
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 58 deletions

View File

@ -9,17 +9,12 @@
import Browse
import DataModels
import Login
import KeychainStorage
import Profile
import SwiftUI
import UseCases
struct ContentView: View {
// MARK: Storages
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
// MARK: States
@State private var user: User?
@ -33,10 +28,6 @@ struct ContentView: View {
var body: some View {
Container(user: user) {
// TODO: create a new folder
} uploadFile: {
// TODO: upload a new file
} showProfile: {
showSheet = .profile
} login: {
showSheet = .login
@ -45,17 +36,18 @@ struct ContentView: View {
switch sheet {
case .login:
LoginView {
account = $0
user = $1
user = $0
}
case .profile:
ProfileView(user: user) {
account = nil
user = nil
}
}
}
.task(id: account) {
.onChange(of: user) {
showSheet = $0 == nil ? .login : nil
}
.task {
await loadUserOrLogin()
}
}
@ -70,8 +62,6 @@ private extension ContentView {
// MARK: Properties
let user: User?
let createFolder: ActionClosure
let uploadFile: ActionClosure
let showProfile: ActionClosure
let login: ActionClosure
@ -102,16 +92,11 @@ private extension ContentView {
private extension ContentView {
func loadUserOrLogin() async {
guard let account else {
showSheet = .login
return
do {
user = try await getUser()
} catch {
// TODO: Handle this error appropriately.
}
showSheet = nil
user = try? await getUser(
username: account.username,
password: account.password
)
}
}

View File

@ -8,7 +8,7 @@
import Foundation
public struct User {
public struct User: Equatable {
// MARK: Properties
@ -30,7 +30,7 @@ public struct User {
// MARK: - Structs
extension User {
public struct Profile {
public struct Profile: Equatable {
// MARK: Properties
@ -49,7 +49,7 @@ extension User {
}
public struct RootFolder {
public struct RootFolder: Equatable {
// MARK: Properties

View File

@ -10,22 +10,76 @@ import APIService
import DataModels
import DependencyInjection
import Dependencies
import KeychainStorage
public struct GetUserUseCase {
public actor GetUserUseCase {
// MARK: Dependencies
// MARK: Properties
@Dependency(\.apiService) private var apiService
private let apiService: APIService
private var account: Account?
// MARK: Initialisers
public init() {}
public init(
apiService: APIService,
account: Account?
) {
self.apiService = apiService
self.account = account
}
// MARK: Functions
public func callAsFunction() async throws -> User {
guard let account else { throw GetUserError .accountNotFound }
return try await getUser(
username: account.username,
password: account.password
)
}
public func callAsFunction(
username: String,
password: String
) async throws -> User {
let user = try await getUser(
username: username,
password: password
)
account = .init(
username: username,
password: password
)
return user
}
}
// MARK: - Initialisers
public extension GetUserUseCase {
init() {
@Dependency(\.apiService) var apiService
@KeychainStorage(key: .KeychainStorage.account) var account: Account?
self.init(
apiService: apiService,
account: account
)
}
}
// MARK: - Helpers
private extension GetUserUseCase {
func getUser(
username: String,
password: String
) async throws -> User {
let me = try await apiService.getUser(
credentials: .init(
@ -46,5 +100,10 @@ public struct GetUserUseCase {
)
)
}
}
// MARK: - Errors
public enum GetUserError: Error {
case accountNotFound
}

View File

@ -34,7 +34,7 @@ public struct LoginView: View {
.vertical,
showsIndicators: false
) {
LoginContainer(authenticated: authenticated)
Container(authenticated)
.padding(.horizontal, 24)
.padding(.top, containerTopPadding)
}
@ -52,7 +52,7 @@ public struct LoginView: View {
// MARK: - Views
fileprivate extension LoginView {
struct LoginContainer: View {
struct Container: View {
// MARK: States
@ -63,9 +63,13 @@ fileprivate extension LoginView {
// MARK: Properties
let authenticated: AuthenticatedClosure
private var getUser: GetUserUseCase = .init()
private let getUser: GetUserUseCase = .init()
private let authenticated: AuthenticatedClosure
init(_ authenticated: @escaping AuthenticatedClosure) {
self.authenticated = authenticated
}
// MARK: Body
@ -73,8 +77,7 @@ fileprivate extension LoginView {
VStack(spacing: 32) {
Text(
"login.title.text",
bundle: .module,
comment: "Login view title text."
bundle: .module
)
.font(.largeTitle)
.fontWeight(.bold)
@ -94,13 +97,13 @@ fileprivate extension LoginView {
Label {
Text(
"login.button.log_in.text",
bundle: .module,
comment: "Log in button text."
bundle: .module
)
.fontWeight(.semibold)
} icon: {
if isAuthenticating {
ProgressView()
.tint(.white)
.controlSize(.regular)
} else {
EmptyView()
@ -123,12 +126,14 @@ fileprivate extension LoginView {
// MARK: - Helpers
private extension LoginView.LoginContainer {
private extension LoginView.Container {
// MARK: Computed
var isLoginDisabled: Bool {
username.isEmpty || password.isEmpty
username.isEmpty
|| password.isEmpty
|| errorMessage != nil
}
// MARK: Functions
@ -136,23 +141,22 @@ private extension LoginView.LoginContainer {
func authenticate() async {
guard isAuthenticating else { return }
defer { isAuthenticating = false }
do {
authenticated(
.init(
username: username,
password: password
),
try await getUser(
username: username,
password: password
)
let user = try await getUser(
username: username,
password: password
)
// Added some throttle (1 second) not to hide the loading indicator right away.
try await Task.sleep(nanoseconds: .Constants.secondInNanoseconds)
authenticated(user)
} catch APIClientError.authenticationFailed {
errorMessage = "login.error.authentication_failed.text"
isAuthenticating = false
} catch {
errorMessage = "login.error.authentication_unknown.text"
isAuthenticating = false
}
}
@ -160,14 +164,22 @@ private extension LoginView.LoginContainer {
// MARK: - Type aliases
public typealias AuthenticatedClosure = (Account, User) -> Void
public typealias AuthenticatedClosure = (User) -> Void
// MARK: - UInt64+Constants
private extension UInt64 {
enum Constants {
static let secondInNanoseconds = UInt64(1 * Double(NSEC_PER_SEC))
}
}
// MARK: - Previews
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView { _, _ in
// closure for authenticated action.
LoginView { _ in
// authenticated closure.
}
}
}