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

View File

@ -10,22 +10,76 @@ import APIService
import DataModels import DataModels
import DependencyInjection import DependencyInjection
import Dependencies 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 // MARK: Initialisers
public init() {} public init(
apiService: APIService,
account: Account?
) {
self.apiService = apiService
self.account = account
}
// MARK: Functions // 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( public func callAsFunction(
username: String, username: String,
password: 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 { ) async throws -> User {
let me = try await apiService.getUser( let me = try await apiService.getUser(
credentials: .init( 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, .vertical,
showsIndicators: false showsIndicators: false
) { ) {
LoginContainer(authenticated: authenticated) Container(authenticated)
.padding(.horizontal, 24) .padding(.horizontal, 24)
.padding(.top, containerTopPadding) .padding(.top, containerTopPadding)
} }
@ -52,7 +52,7 @@ public struct LoginView: View {
// MARK: - Views // MARK: - Views
fileprivate extension LoginView { fileprivate extension LoginView {
struct LoginContainer: View { struct Container: View {
// MARK: States // MARK: States
@ -60,12 +60,16 @@ fileprivate extension LoginView {
@State private var username: String = "" @State private var username: String = ""
@State private var password: String = "" @State private var password: String = ""
@State private var errorMessage: String? @State private var errorMessage: String?
// MARK: Properties // MARK: Properties
private var getUser: GetUserUseCase = .init()
let authenticated: AuthenticatedClosure private let authenticated: AuthenticatedClosure
private let getUser: GetUserUseCase = .init() init(_ authenticated: @escaping AuthenticatedClosure) {
self.authenticated = authenticated
}
// MARK: Body // MARK: Body
@ -73,8 +77,7 @@ fileprivate extension LoginView {
VStack(spacing: 32) { VStack(spacing: 32) {
Text( Text(
"login.title.text", "login.title.text",
bundle: .module, bundle: .module
comment: "Login view title text."
) )
.font(.largeTitle) .font(.largeTitle)
.fontWeight(.bold) .fontWeight(.bold)
@ -94,13 +97,13 @@ fileprivate extension LoginView {
Label { Label {
Text( Text(
"login.button.log_in.text", "login.button.log_in.text",
bundle: .module, bundle: .module
comment: "Log in button text."
) )
.fontWeight(.semibold) .fontWeight(.semibold)
} icon: { } icon: {
if isAuthenticating { if isAuthenticating {
ProgressView() ProgressView()
.tint(.white)
.controlSize(.regular) .controlSize(.regular)
} else { } else {
EmptyView() EmptyView()
@ -123,12 +126,14 @@ fileprivate extension LoginView {
// MARK: - Helpers // MARK: - Helpers
private extension LoginView.LoginContainer { private extension LoginView.Container {
// MARK: Computed // MARK: Computed
var isLoginDisabled: Bool { var isLoginDisabled: Bool {
username.isEmpty || password.isEmpty username.isEmpty
|| password.isEmpty
|| errorMessage != nil
} }
// MARK: Functions // MARK: Functions
@ -136,23 +141,22 @@ private extension LoginView.LoginContainer {
func authenticate() async { func authenticate() async {
guard isAuthenticating else { return } guard isAuthenticating else { return }
defer { isAuthenticating = false }
do { do {
authenticated( let user = try await getUser(
.init( username: username,
username: username, password: password
password: password
),
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 { } catch APIClientError.authenticationFailed {
errorMessage = "login.error.authentication_failed.text" errorMessage = "login.error.authentication_failed.text"
isAuthenticating = false
} catch { } catch {
errorMessage = "login.error.authentication_unknown.text" errorMessage = "login.error.authentication_unknown.text"
isAuthenticating = false
} }
} }
@ -160,14 +164,22 @@ private extension LoginView.LoginContainer {
// MARK: - Type aliases // 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 // MARK: - Previews
struct LoginView_Previews: PreviewProvider { struct LoginView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
LoginView { _, _ in LoginView { _ in
// closure for authenticated action. // authenticated closure.
} }
} }
} }