From 76814cfe8bbdcd84f028c457189145cd68e16e47 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 18 Dec 2022 15:06:38 +0100 Subject: [PATCH 1/3] Fixed the progress indicator in the login button of the LoginView view for the Login module. --- .../Sources/Login/UI/Views/LoginView.swift | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index a88dade..e2977b4 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -73,8 +73,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 +93,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() @@ -128,7 +127,9 @@ private extension LoginView.LoginContainer { // MARK: Computed var isLoginDisabled: Bool { - username.isEmpty || password.isEmpty + username.isEmpty + || password.isEmpty + || errorMessage != nil } // MARK: Functions @@ -136,23 +137,28 @@ private extension LoginView.LoginContainer { func authenticate() async { guard isAuthenticating else { return } + defer { isAuthenticating = false } + do { + 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( .init( username: username, password: password ), - try await getUser( - username: username, - password: password - ) + user ) } catch APIClientError.authenticationFailed { errorMessage = "login.error.authentication_failed.text" - isAuthenticating = false } catch { errorMessage = "login.error.authentication_unknown.text" - isAuthenticating = false } } @@ -162,6 +168,14 @@ private extension LoginView.LoginContainer { public typealias AuthenticatedClosure = (Account, 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 { From c25ca4cf0fade840f9399614696ccb84a4a3f7e0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 18 Dec 2022 16:12:09 +0100 Subject: [PATCH 2/3] Improved the GetUserUseCase use case by moving some logic to this use case. --- .../UseCases/Users/GetUserUseCase.swift | 69 +++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/Libraries/Sources/UseCases/Users/GetUserUseCase.swift b/Libraries/Sources/UseCases/Users/GetUserUseCase.swift index e06aeae..fe2eaed 100644 --- a/Libraries/Sources/UseCases/Users/GetUserUseCase.swift +++ b/Libraries/Sources/UseCases/Users/GetUserUseCase.swift @@ -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 } From 9045853db96d4460bcdb7afabbb305d61b44f1f6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 18 Dec 2022 16:12:56 +0100 Subject: [PATCH 3/3] Updated the LoginView and the ContentView views based on the changes to the GetuseUseCase use case. --- BeReal/UI/Views/ContentView.swift | 35 ++++++------------- .../Sources/DataModels/Models/User.swift | 6 ++-- .../Sources/Login/UI/Views/LoginView.swift | 30 ++++++++-------- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index 4af869e..a9316c6 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -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 - ) } } diff --git a/Libraries/Sources/DataModels/Models/User.swift b/Libraries/Sources/DataModels/Models/User.swift index 6a5e1cc..119aa48 100644 --- a/Libraries/Sources/DataModels/Models/User.swift +++ b/Libraries/Sources/DataModels/Models/User.swift @@ -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 diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index e2977b4..9a32cd1 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -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 @@ -60,12 +60,16 @@ fileprivate extension LoginView { @State private var username: String = "" @State private var password: String = "" @State private var errorMessage: String? - + // 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 @@ -122,7 +126,7 @@ fileprivate extension LoginView { // MARK: - Helpers -private extension LoginView.LoginContainer { +private extension LoginView.Container { // MARK: Computed @@ -148,13 +152,7 @@ private extension LoginView.LoginContainer { // Added some throttle (1 second) not to hide the loading indicator right away. try await Task.sleep(nanoseconds: .Constants.secondInNanoseconds) - authenticated( - .init( - username: username, - password: password - ), - user - ) + authenticated(user) } catch APIClientError.authenticationFailed { errorMessage = "login.error.authentication_failed.text" } catch { @@ -166,7 +164,7 @@ private extension LoginView.LoginContainer { // MARK: - Type aliases -public typealias AuthenticatedClosure = (Account, User) -> Void +public typealias AuthenticatedClosure = (User) -> Void // MARK: - UInt64+Constants @@ -180,8 +178,8 @@ private extension UInt64 { struct LoginView_Previews: PreviewProvider { static var previews: some View { - LoginView { _, _ in - // closure for authenticated action. + LoginView { _ in + // authenticated closure. } } }