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 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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user