// // LoginView.swift // Login // // Created by Javier Cicchelli on 30/11/2022. // Copyright © 2022 Röck+Cöde. All rights reserved. // import APIService import DataModels import SwiftUI import UseCases public struct LoginView: View { // MARK: States @State private var containerTopPadding: CGFloat = 0 // MARK: Properties private let authenticated: AuthenticatedClosure // MARK: Initialisers public init(authenticated: @escaping AuthenticatedClosure) { self.authenticated = authenticated } // MARK: Body public var body: some View { ScrollView( .vertical, showsIndicators: false ) { LoginContainer(authenticated: authenticated) .padding(.horizontal, 24) .padding(.top, containerTopPadding) } .background(Color.primary.colorInvert()) .ignoresSafeArea() .overlay(ViewHeightGeometry()) .onPreferenceChange(ViewHeightPreferenceKey.self) { height in containerTopPadding = height * 0.1 } .interactiveDismissDisabled() } } // MARK: - Views fileprivate extension LoginView { struct LoginContainer: View { // MARK: States @State private var isAuthenticating: Bool = false @State private var username: String = "" @State private var password: String = "" @State private var errorMessage: String? // MARK: Properties let authenticated: AuthenticatedClosure private let getUser: GetUserUseCase = .init() // MARK: Body var body: some View { VStack(spacing: 32) { Text( "login.title.text", bundle: .module ) .font(.largeTitle) .fontWeight(.bold) .foregroundColor(.primary) LoginForm( username: $username, password: $password, errorMessage: $errorMessage ) { isAuthenticating = true } Button { isAuthenticating = true } label: { Label { Text( "login.button.log_in.text", bundle: .module ) .fontWeight(.semibold) } icon: { if isAuthenticating { ProgressView() .tint(.white) .controlSize(.regular) } else { EmptyView() } } .labelStyle(.logIn) } .tint(.red) .buttonStyle(.borderedProminent) .buttonBorderShape(.roundedRectangle(radius: 8)) .controlSize(.large) .disabled(isLoginDisabled) .task(id: isAuthenticating) { await authenticate() } } } } } // MARK: - Helpers private extension LoginView.LoginContainer { // MARK: Computed var isLoginDisabled: Bool { username.isEmpty || password.isEmpty || errorMessage != nil } // MARK: Functions 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 ), user ) } catch APIClientError.authenticationFailed { errorMessage = "login.error.authentication_failed.text" } catch { errorMessage = "login.error.authentication_unknown.text" } } } // MARK: - Type aliases 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 { static var previews: some View { LoginView { _, _ in // closure for authenticated action. } } }