diff --git a/BeReal.xcodeproj/project.pbxproj b/BeReal.xcodeproj/project.pbxproj index 6adce31..e9c0e8f 100644 --- a/BeReal.xcodeproj/project.pbxproj +++ b/BeReal.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 027F605B2937663400467238 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027F605A2937663400467238 /* LoginView.swift */; }; 02AE64EF29363DBF005A4AF3 /* BeRealApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */; }; 02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64F029363DBF005A4AF3 /* ContentView.swift */; }; 02AE64F329363DC1005A4AF3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 02AE64F229363DC1005A4AF3 /* Assets.xcassets */; }; @@ -14,6 +15,8 @@ 02AE650029363DC1005A4AF3 /* BeRealTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64FF29363DC1005A4AF3 /* BeRealTests.swift */; }; 02AE650A29363DC1005A4AF3 /* BeRealUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE650929363DC1005A4AF3 /* BeRealUITests.swift */; }; 02AE650C29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */; }; + 02FFFD7829395D8C00306533 /* LoginForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFFD7729395D8C00306533 /* LoginForm.swift */; }; + 02FFFD7B29395DD200306533 /* String+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFFD7A29395DD200306533 /* String+Constants.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,6 +37,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 027F605A2937663400467238 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 02AE64EB29363DBF005A4AF3 /* BeReal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BeReal.app; sourceTree = BUILT_PRODUCTS_DIR; }; 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealApp.swift; sourceTree = ""; }; 02AE64F029363DBF005A4AF3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -44,6 +48,8 @@ 02AE650529363DC1005A4AF3 /* BeRealUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BeRealUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02AE650929363DC1005A4AF3 /* BeRealUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITests.swift; sourceTree = ""; }; 02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITestsLaunchTests.swift; sourceTree = ""; }; + 02FFFD7729395D8C00306533 /* LoginForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginForm.swift; sourceTree = ""; }; + 02FFFD7A29395DD200306533 /* String+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Constants.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -71,6 +77,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 027F60592937662300467238 /* Login */ = { + isa = PBXGroup; + children = ( + 02FFFD7629395D7F00306533 /* Components */, + 027F605C29376EEB00467238 /* Views */, + ); + path = Login; + sourceTree = ""; + }; + 027F605C29376EEB00467238 /* Views */ = { + isa = PBXGroup; + children = ( + 027F605A2937663400467238 /* LoginView.swift */, + ); + path = Views; + sourceTree = ""; + }; 02AE64E229363DBF005A4AF3 = { isa = PBXGroup; children = ( @@ -94,6 +117,8 @@ 02AE64ED29363DBF005A4AF3 /* BeReal */ = { isa = PBXGroup; children = ( + 02FFFD7929395DBF00306533 /* Extensions */, + 027F60592937662300467238 /* Login */, 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */, 02AE64F029363DBF005A4AF3 /* ContentView.swift */, 02AE64F229363DC1005A4AF3 /* Assets.xcassets */, @@ -127,6 +152,22 @@ path = BeRealUITests; sourceTree = ""; }; + 02FFFD7629395D7F00306533 /* Components */ = { + isa = PBXGroup; + children = ( + 02FFFD7729395D8C00306533 /* LoginForm.swift */, + ); + path = Components; + sourceTree = ""; + }; + 02FFFD7929395DBF00306533 /* Extensions */ = { + isa = PBXGroup; + children = ( + 02FFFD7A29395DD200306533 /* String+Constants.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -192,6 +233,7 @@ BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; LastUpgradeCheck = 1410; + ORGANIZATIONNAME = "Röck+Cöde"; TargetAttributes = { 02AE64EA29363DBF005A4AF3 = { CreatedOnToolsVersion = 14.1; @@ -257,8 +299,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 027F605B2937663400467238 /* LoginView.swift in Sources */, 02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */, 02AE64EF29363DBF005A4AF3 /* BeRealApp.swift in Sources */, + 02FFFD7829395D8C00306533 /* LoginForm.swift in Sources */, + 02FFFD7B29395DD200306533 /* String+Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BeReal/ContentView.swift b/BeReal/ContentView.swift index a00be8d..7790b30 100644 --- a/BeReal/ContentView.swift +++ b/BeReal/ContentView.swift @@ -17,6 +17,9 @@ struct ContentView: View { Text("Hello, world!") } .padding() + .sheet(isPresented: .constant(true)) { + LoginView() + } } } diff --git a/BeReal/Extensions/String+Constants.swift b/BeReal/Extensions/String+Constants.swift new file mode 100644 index 0000000..b4df93d --- /dev/null +++ b/BeReal/Extensions/String+Constants.swift @@ -0,0 +1,11 @@ +// +// String+Constants.swift +// BeReal +// +// Created by Javier Cicchelli on 01/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +extension String { + static let empty = "" +} diff --git a/BeReal/Login/Components/LoginForm.swift b/BeReal/Login/Components/LoginForm.swift new file mode 100644 index 0000000..024f080 --- /dev/null +++ b/BeReal/Login/Components/LoginForm.swift @@ -0,0 +1,92 @@ +// +// LoginForm.swift +// BeReal +// +// Created by Javier Cicchelli on 01/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import SwiftUI + +struct LoginForm: View { + + // MARK: Bindings + + @Binding var username: String + @Binding var password: String + @Binding var errorMessage: String? + + // MARK: Body + + var body: some View { + VStack( + alignment: .leading, + spacing: 16 + ) { + TextField( + "Username", + text: $username + ) + .textContentType(.username) + .autocapitalization(.none) + .disableAutocorrection(true) + .keyboardType(.default) + + Divider() + + SecureField( + "Password", + text: $password + ) + .textContentType(.password) + .autocapitalization(.none) + .disableAutocorrection(true) + .keyboardType(.default) + + if let errorMessage { + Divider() + + Text(errorMessage) + .font(.body) + .foregroundColor(.red) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + .background(Color.white) + .cornerRadius(8) + .onAppear { + setClearButtonIfNeeded() + } + } +} + +// MARK: - Helpers + +private extension LoginForm { + func setClearButtonIfNeeded() { + guard UITextField.appearance().clearButtonMode != .whileEditing else { return } + + UITextField.appearance().clearButtonMode = .whileEditing + } +} + +// MARK: - Previews + +struct LoginForm_Previews: PreviewProvider { + static var previews: some View { + LoginForm( + username: .constant("Some username"), + password: .constant("Some Password"), + errorMessage: .constant(nil) + ) + .previewDisplayName("Login form with no error message") + + LoginForm( + username: .constant("Some username"), + password: .constant("Some Password"), + errorMessage: .constant("Some error goes in here...") + ) + .previewDisplayName("Login form with some error message") + } +} diff --git a/BeReal/Login/Views/LoginView.swift b/BeReal/Login/Views/LoginView.swift new file mode 100644 index 0000000..3e2cadc --- /dev/null +++ b/BeReal/Login/Views/LoginView.swift @@ -0,0 +1,103 @@ +// +// LoginView.swift +// BeReal +// +// Created by Javier Cicchelli on 30/11/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import SwiftUI + +struct LoginView: View { + + // MARK: States + + @State private var topPadding: CGFloat = 0 + + // MARK: Body + + var body: some View { + ScrollView( + .vertical, + showsIndicators: false + ) { + Container() + .padding(.horizontal, 24) + .padding(.top, topPadding) + } + .background(Color.red) + .ignoresSafeArea() + .overlay(ViewHeightGeometry()) + .onPreferenceChange(ViewHeightPreferenceKey.self) { height in + topPadding = height * 0.25 + } + } + +} + +// MARK: - Views + +fileprivate extension LoginView { + struct Container: View { + @State private var username: String = .empty + @State private var password: String = .empty + @State private var errorMessage: String? + + var body: some View { + VStack(spacing: 24) { + Text("My NFS") + .font(.largeTitle) + .fontWeight(.bold) + + LoginForm( + username: $username, + password: $password, + errorMessage: $errorMessage + ) + + Button { + // ... + } label: { + Text("Log in") + .font(.body) + .fontWeight(.semibold) + .padding(.vertical, 8) + .padding(.horizontal, 32) + .background( + Capsule() + .foregroundColor(.white) + ) + } + } + } + } + + struct ViewHeightGeometry: View { + var body: some View { + GeometryReader { proxy in + Color.clear.preference( + key: ViewHeightPreferenceKey.self, + value: proxy.size.height + proxy.safeAreaInsets.top + proxy.safeAreaInsets.bottom + ) + } + } + } +} + +// MARK: - Preference keys + +struct ViewHeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +// MARK: - Previews + +struct LoginView_Previews: PreviewProvider { + static var previews: some View { + LoginView() + } +}