Merge pull request #19 from rock-n-code/improvements/progress-indicator-login
Improvement: Progress indicator in the Login view
This commit is contained in:
commit
ca5072e660
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user