diff --git a/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift b/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift index e3bac5be..fae1762d 100644 --- a/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift +++ b/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift @@ -10,6 +10,7 @@ public extension TargetDependency { } public extension TargetDependency.Project.Features { + static let SigninFeature = TargetDependency.feature(name: "SigninFeature") static let BaseFeature = TargetDependency.feature(name: "BaseFeature") static let RootFeature = TargetDependency.feature(name: "RootFeature") } diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index 7c90feb1..432802c2 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -1,6 +1,7 @@ import NeedleFoundation import SwiftUI import KeychainModule +import SigninFeature public final class AppComponent: BootstrapComponent { public func makeRootView() -> some View { @@ -11,3 +12,9 @@ public final class AppComponent: BootstrapComponent { KeychainImpl() } } + +public extension AppComponent { + var signinComponent: SigninComponent { + SigninComponent(parent: self) + } +} diff --git a/Projects/App/Sources/Application/DMSApp.swift b/Projects/App/Sources/Application/DMSApp.swift index a151d9ae..462dbda2 100644 --- a/Projects/App/Sources/Application/DMSApp.swift +++ b/Projects/App/Sources/Application/DMSApp.swift @@ -1,11 +1,18 @@ import SwiftUI import DesignSystem +import SigninFeature @main struct DMSApp: App { + init() { + registerProviderFactories() + } + var body: some Scene { WindowGroup { - DesignSystemPlaygroundView() + NavigationView { + AppComponent().signinComponent.makeView() + } } } } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index af5d4a11..fd2007e1 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -5,6 +5,7 @@ import DomainModule import KeychainModule import NeedleFoundation import NetworkModule +import SigninFeature import SwiftUI // swiftlint:disable unused_declaration @@ -20,11 +21,46 @@ private func parent1(_ component: NeedleFoundation.Scope) -> NeedleFoundation.Sc #if !NEEDLE_DYNAMIC +private class SigninDependencyde06a9d0b22764487733Provider: SigninDependency { + var signinUseCase: any SigninUseCase { + return appComponent.signinUseCase + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->SigninComponent +private func factory2882a056d84a613debccf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return SigninDependencyde06a9d0b22764487733Provider(appComponent: parent1(component) as! AppComponent) +} #else extension AppComponent: Registration { public func registerItems() { + localTable["keychain-any Keychain"] = { self.keychain as Any } + localTable["remoteAuthDataSource-any RemoteAuthDataSource"] = { self.remoteAuthDataSource as Any } + localTable["authRepository-any AuthRepository"] = { self.authRepository as Any } + localTable["signinUseCase-any SigninUseCase"] = { self.signinUseCase as Any } + localTable["verifyAuthCodeUseCase-any VerifyAuthCodeUseCase"] = { self.verifyAuthCodeUseCase as Any } + localTable["sendAuthCodeUseCase-any SendAuthCodeUseCase"] = { self.sendAuthCodeUseCase as Any } + localTable["checkEmailExistByAccountIDUseCase-any CheckEmailExistByAccountIDUseCase"] = { self.checkEmailExistByAccountIDUseCase as Any } + localTable["checkAccountIDIsExistUseCase-any CheckAccountIDIsExistUseCase"] = { self.checkAccountIDIsExistUseCase as Any } + localTable["signinComponent-SigninComponent"] = { self.signinComponent as Any } + localTable["remoteStudentsDataSource-any RemoteStudentsDataSource"] = { self.remoteStudentsDataSource as Any } + localTable["studentsRepository-any StudentsRepository"] = { self.studentsRepository as Any } + localTable["signupUseCase-any SignupUseCase"] = { self.signupUseCase as Any } + localTable["checkDuplicateAccountIDUseCase-any CheckDuplicateAccountIDUseCase"] = { self.checkDuplicateAccountIDUseCase as Any } + localTable["checkDuplicateEmailUseCase-any CheckDuplicateEmailUseCase"] = { self.checkDuplicateEmailUseCase as Any } + localTable["renewalPasswordUseCase-any RenewalPasswordUseCase"] = { self.renewalPasswordUseCase as Any } + localTable["findIDUseCase-any FindIDUseCase"] = { self.findIDUseCase as Any } + localTable["fetchMyProfileUseCase-any FetchMyProfileUseCase"] = { self.fetchMyProfileUseCase as Any } + } +} +extension SigninComponent: Registration { + public func registerItems() { + keyPathToName[\SigninDependency.signinUseCase] = "signinUseCase-any SigninUseCase" } } @@ -44,6 +80,7 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi private func register1() { registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) + registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) } #endif diff --git a/Projects/Features/RootFeature/Project.swift b/Projects/Features/RootFeature/Project.swift index 88cafc68..4da1352b 100644 --- a/Projects/Features/RootFeature/Project.swift +++ b/Projects/Features/RootFeature/Project.swift @@ -5,6 +5,7 @@ let project = Project.makeModule( name: "RootFeature", product: .staticFramework, dependencies: [ - .Project.Features.BaseFeature + .Project.Features.BaseFeature, + .Project.Features.SigninFeature ] ) diff --git a/Projects/Features/SigninFeature/Derived/InfoPlists/SigninFeature-Info.plist b/Projects/Features/SigninFeature/Derived/InfoPlists/SigninFeature-Info.plist new file mode 100644 index 00000000..323e5ecf --- /dev/null +++ b/Projects/Features/SigninFeature/Derived/InfoPlists/SigninFeature-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Projects/Features/SigninFeature/Derived/InfoPlists/SigninFeatureTests-Info.plist b/Projects/Features/SigninFeature/Derived/InfoPlists/SigninFeatureTests-Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/Projects/Features/SigninFeature/Derived/InfoPlists/SigninFeatureTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Projects/Features/SigninFeature/Project.swift b/Projects/Features/SigninFeature/Project.swift new file mode 100644 index 00000000..564617bd --- /dev/null +++ b/Projects/Features/SigninFeature/Project.swift @@ -0,0 +1,10 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeModule( + name: "SigninFeature", + product: .staticFramework, + dependencies: [ + .Project.Features.BaseFeature + ] +) diff --git a/Projects/Features/SigninFeature/Sources/SigninComponent.swift b/Projects/Features/SigninFeature/Sources/SigninComponent.swift new file mode 100644 index 00000000..f64cdfed --- /dev/null +++ b/Projects/Features/SigninFeature/Sources/SigninComponent.swift @@ -0,0 +1,17 @@ +import DomainModule +import NeedleFoundation +import SwiftUI + +public protocol SigninDependency: Dependency { + var signinUseCase: any SigninUseCase { get } +} + +public final class SigninComponent: Component { + public func makeView() -> some View { + SigninView( + viewModel: .init( + signinUseCase: dependency.signinUseCase + ) + ) + } +} diff --git a/Projects/Features/SigninFeature/Sources/SigninView.swift b/Projects/Features/SigninFeature/Sources/SigninView.swift new file mode 100644 index 00000000..b678d3ca --- /dev/null +++ b/Projects/Features/SigninFeature/Sources/SigninView.swift @@ -0,0 +1,117 @@ +import SwiftUI +import DesignSystem + +struct SigninView: View { + private enum FocusField { + case id + case password + } + @StateObject var viewModel: SigninViewModel + @FocusState private var focusField: FocusField? + + public init(viewModel: SigninViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + VStack { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("DMS") + .dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary) + .padding(.top, 28) + + Text("더 편한 기숙사 생활을 위해") + .dmsFont(.text(.medium), color: .GrayScale.gray6) + } + + Spacer() + } + + VStack(spacing: 72) { + DMSFloatingTextField( + "아이디", + text: $viewModel.id, + isError: viewModel.isErrorOcuured + ) { + focusField = .password + } + .textContentType(.username) + .focused($focusField, equals: .id) + + SecureDMSFloatingTextField("비밀번호", text: $viewModel.password) { + viewModel.signinButtonDidTap() + } + .textContentType(.password) + .focused($focusField, equals: .password) + } + .padding(.top, 68) + + HStack(spacing: 16) { + HStack(spacing: 12) { + DMSRadioButton(isOn: $viewModel.isOnAutoSignin) + + Text("자동로그인") + .dmsFont(.text(.small), color: .GrayScale.gray6) + } + .onTapGesture { + withAnimation { + viewModel.isOnAutoSignin.toggle() + } + } + + Spacer() + + NavigationLink { + Text("아이디 찾기") + } label: { + Text("아이디 찾기") + .dmsFont(.text(.extraSmall), color: .GrayScale.gray5) + } + + Divider() + .foregroundColor(.GrayScale.gray5) + .frame(height: 13) + + NavigationLink { + Text("비밀번호 재설정") + } label: { + Text("비밀번호 재설정") + .dmsFont(.text(.extraSmall), color: .GrayScale.gray5) + } + } + .padding(.top, 16) + + Spacer() + + HStack(spacing: 16) { + Text("아직 회원이 아니신가요?") + .dmsFont(.text(.extraSmall), color: .GrayScale.gray5) + + DMSButton(text: "회원가입", style: .text, color: .GrayScale.gray6) { + viewModel.isNavigateSignup.toggle() + } + } + + DMSWideButton(text: "로그인", color: .PrimaryVariant.primary) { + viewModel.signinButtonDidTap() + } + .disabled(!viewModel.isSigninButtonEnabled) + .padding(.top, 24) + .frame(maxWidth: .infinity) + .padding(.bottom, 40) + } + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) + .frame(maxWidth: .infinity) + .padding(.horizontal, 24) + .dmsBackground() + .navigate(to: Text("회원가입"), when: $viewModel.isNavigateSignup) + .ignoresSafeArea(.keyboard, edges: .bottom) + } +} + +struct SigninView_Previews: PreviewProvider { + static var previews: some View { + Text("A") + } +} diff --git a/Projects/Features/SigninFeature/Sources/SigninViewModel.swift b/Projects/Features/SigninFeature/Sources/SigninViewModel.swift new file mode 100644 index 00000000..502240d1 --- /dev/null +++ b/Projects/Features/SigninFeature/Sources/SigninViewModel.swift @@ -0,0 +1,27 @@ +import BaseFeature +import Combine +import DomainModule + +final class SigninViewModel: BaseViewModel { + @Published var id = "" + @Published var password = "" + @Published var isOnAutoSignin = true + @Published var isNavigateSignup = false + @Published var isSuccessSignin = false + var isSigninButtonEnabled: Bool { + !id.isEmpty && !password.isEmpty + } + + private let signinUseCase: any SigninUseCase + + public init(signinUseCase: any SigninUseCase) { + self.signinUseCase = signinUseCase + } + + func signinButtonDidTap() { + guard isSigninButtonEnabled else { return } + addCancellable(signinUseCase.execute(req: .init(accountID: id, password: password))) { [weak self] _ in + self?.isSuccessSignin = true + } + } +} diff --git a/Projects/Features/SigninFeature/Tests/SigninViewModelSpec.swift b/Projects/Features/SigninFeature/Tests/SigninViewModelSpec.swift new file mode 100644 index 00000000..83709c66 --- /dev/null +++ b/Projects/Features/SigninFeature/Tests/SigninViewModelSpec.swift @@ -0,0 +1,85 @@ +import Quick +import Nimble +import Combine +import DomainModule +import DataModule +@testable import SigninFeature + +// swiftlint: disable function_body_length +final class SigninViewModelSpec: QuickSpec { + override func spec() { + var signinUseCase: SigninUseCase! + var sut: SigninViewModel! + + beforeEach { + signinUseCase = SigninUseCaseFake() + sut = SigninViewModel(signinUseCase: signinUseCase) + } + describe("SigninViewModel에서") { + context("유저가 아무것도 입력하지 않은 상태라면") { + it("isSigninButtonEnabled은 false이다.") { + expect { sut.id }.to(beEmpty()) + expect { sut.password }.to(beEmpty()) + expect { sut.isSigninButtonEnabled }.to(beFalse()) + } + } + context("유저가 ID만 입력했다면") { + beforeEach { + sut.id = "A" + } + it("isSigninButtonEnabled은 false이다.") { + expect { sut.id }.toNot(beEmpty()) + expect { sut.password }.to(beEmpty()) + expect { sut.isSigninButtonEnabled }.to(beFalse()) + } + } + context("유저가 Password만 입력했다면") { + beforeEach { + sut.password = "A" + } + it("isSigninButtonEnabled은 false이다.") { + expect { sut.id }.to(beEmpty()) + expect { sut.password }.toNot(beEmpty()) + expect { sut.isSigninButtonEnabled }.to(beFalse()) + } + } + context("유저가 ID와 Password를 모두 입력했다면") { + beforeEach { + sut.id = "A" + sut.password = "A" + } + it("isSigninButtonEnabled은 true이다.") { + expect { sut.id }.toNot(beEmpty()) + expect { sut.password }.toNot(beEmpty()) + expect { sut.isSigninButtonEnabled }.to(beTrue()) + } + } + context("유저가 ID와 Password를 알맞게 입력하고 로그인을 시도한다면") { + beforeEach { + sut.id = "baekteun" + sut.password = "baekteun" + sut.signinButtonDidTap() + } + it("isSuccessSignin이 true이다.") { + expect { sut.isSuccessSignin }.toEventually(beTrue()) + } + } + context("유저가 ID와 Password중 하나라도 알맞지 않게 입력하고 로그인을 시도한다면") { + beforeEach { + sut.id = "ASDF" + sut.password = "ASDF" + sut.signinButtonDidTap() + } + it("isSuccessSignin은 false이다") { + expect { sut.isSuccessSignin }.toEventually(beFalse()) + } + it("isErrorOcuured는 true이다") { + expect { sut.isErrorOcuured }.toEventually(beTrue()) + } + it("errorMessage는 '아이디 또는 비밀번호를 확인해주세요.'가 나온다") { + expect { sut.errorMessage }.toEventually(equal("아이디 또는 비밀번호를 확인해주세요.")) + } + } + } + } +} diff --git a/Projects/Services/DataModule/Sources/Auth/UseCases/Fake/SigninUseCaseFake.swift b/Projects/Services/DataModule/Sources/Auth/UseCases/Fake/SigninUseCaseFake.swift new file mode 100644 index 00000000..da4db55a --- /dev/null +++ b/Projects/Services/DataModule/Sources/Auth/UseCases/Fake/SigninUseCaseFake.swift @@ -0,0 +1,21 @@ +import Combine +import DataMappingModule +import DomainModule +import ErrorModule +import Foundation + +public struct SigninUseCaseFake: SigninUseCase { + public init () {} + + public func execute(req: SigninRequestDTO) -> AnyPublisher { + if req.accountID == "baekteun" && req.password == "baekteun" { + return Just(()).setFailureType(to: DmsError.self) + .delay(for: 1, scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + } else { + return Fail(error: DmsError.passwordMismatch) + .delay(for: 1, scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + } + } +} diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 1.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 1.colorset/Contents.json index 22c4bb0a..04256378 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 1.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 1.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 2.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 2.colorset/Contents.json index 6b0a608f..4db45498 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 2.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 2.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.976", - "red" : "0.976" + "blue" : "0.071", + "green" : "0.071", + "red" : "0.071" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 3.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 3.colorset/Contents.json index db155404..c95d2c62 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 3.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 3.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.933", - "green" : "0.933", - "red" : "0.933" + "blue" : "0.204", + "green" : "0.204", + "red" : "0.204" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 4.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 4.colorset/Contents.json index f504201b..79d164b4 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 4.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 4.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.867", - "green" : "0.867", - "red" : "0.867" + "blue" : "0.333", + "green" : "0.333", + "red" : "0.333" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 6.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 6.colorset/Contents.json index 47a7c537..f4efbefa 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 6.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 6.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.333", - "green" : "0.333", - "red" : "0.333" + "blue" : "0.867", + "green" : "0.867", + "red" : "0.867" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 7.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 7.colorset/Contents.json index 3f3264b0..818c0309 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 7.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 7.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.204", - "green" : "0.204", - "red" : "0.204" + "blue" : "0.933", + "green" : "0.933", + "red" : "0.933" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 8.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 8.colorset/Contents.json index a04f4b42..40840fdd 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 8.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 8.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.071", - "green" : "0.071", - "red" : "0.071" + "blue" : "0.976", + "green" : "0.976", + "red" : "0.976" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 9.colorset/Contents.json b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 9.colorset/Contents.json index 784f6038..d8907191 100644 --- a/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 9.colorset/Contents.json +++ b/Projects/UsertInterfaces/DesignSystem/Resources/PrimaryColor.xcassets/Gray 9.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.000", - "red" : "0.000" + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" } }, "idiom" : "universal" diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/Extensions/View+navigate.swift b/Projects/UsertInterfaces/DesignSystem/Sources/Extensions/View+navigate.swift new file mode 100644 index 00000000..4a91d416 --- /dev/null +++ b/Projects/UsertInterfaces/DesignSystem/Sources/Extensions/View+navigate.swift @@ -0,0 +1,34 @@ +import SwiftUI + +public extension View { + func navigate( + to view: NewView, + when binding: Binding, + isDetailLink: Bool = false + ) -> some View { + self.background { + NavigationLink(isActive: binding) { + DeferView { + view + } + } label: { + EmptyView() + } + .isDetailLink(isDetailLink) + } + } +} + +public struct DeferView: View { + let content: () -> Content + + public init( + @ViewBuilder _ content: @escaping () -> Content + ) { + self.content = content + } + + public var body: some View { + content() + } +} diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift b/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift index c60b882a..2b6c5137 100644 --- a/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift +++ b/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift @@ -8,6 +8,7 @@ public struct DMSFloatingTextField: View { var errorMessage: String var onCommit: () -> Void @FocusState var isFocused: Bool + @Namespace var animation private var isFloaintg: Bool { isFocused || !text.isEmpty } @@ -41,8 +42,9 @@ public struct DMSFloatingTextField: View { public var body: some View { ZStack(alignment: .leading) { Text(label) - .dmsFont(.text(isFloaintg ? .medium : .extraLarge), color: dmsForegroundColor) + .dmsFont(.text(.extraLarge), color: dmsForegroundColor) .offset(y: isFloaintg ? -40 : isErrorOrHelpNotEmpty ? -10 : 0) + .scaleEffect(isFloaintg ? 0.8 : 1, anchor: .topLeading) VStack(alignment: .leading, spacing: 10) { TextField("", text: $text) @@ -57,9 +59,13 @@ public struct DMSFloatingTextField: View { .focused($isFocused) .onSubmit(onCommit) - if isErrorOrHelpNotEmpty { - Text(isError ? errorMessage : helpMessage) - .dmsFont(.text(.extraSmall), color: isError ? .System.error : .GrayScale.gray5) + if !isError && !helpMessage.isEmpty { + Text(helpMessage) + .dmsFont(.text(.extraSmall), color: .GrayScale.gray5) + .offset(x: 5) + } else if isError && !errorMessage.isEmpty { + Text(errorMessage) + .dmsFont(.text(.extraSmall), color: .System.error) .offset(x: 5) } } diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/SecureDMSFloatingTextField.swift b/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/SecureDMSFloatingTextField.swift index fc083b9d..d1c1d05a 100644 --- a/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/SecureDMSFloatingTextField.swift +++ b/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/SecureDMSFloatingTextField.swift @@ -43,8 +43,9 @@ public struct SecureDMSFloatingTextField: View { ZStack(alignment: .leading) { HStack { Text(label) - .dmsFont(.text(isFloaintg ? .medium : .extraLarge), color: dmsForegroundColor) + .dmsFont(.text(.extraLarge), color: dmsForegroundColor) .offset(y: isFloaintg ? -40 : isErrorOrHelpNotEmpty ? -10 : 0) + .scaleEffect(isFloaintg ? 0.8 : 1, anchor: .topLeading) .onTapGesture { isFocused = true }