Merge pull request #1 from rock-n-code/login

Feature: Login UI
This commit is contained in:
Javier Cicchelli 2022-12-03 06:12:57 +01:00 committed by GitHub
commit b790c03adf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 438 additions and 0 deletions

View File

@ -14,6 +14,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 */; };
02FFFD7B29395DD200306533 /* String+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFFD7A29395DD200306533 /* String+Constants.swift */; };
4694AAA0293A7C8800D54903 /* Modules in Frameworks */ = {isa = PBXBuildFile; productRef = 4694AA9F293A7C8800D54903 /* Modules */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -34,6 +36,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
02784F03293A8331005F839D /* Modules */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Modules; sourceTree = "<group>"; };
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 = "<group>"; };
02AE64F029363DBF005A4AF3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -44,6 +47,7 @@
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 = "<group>"; };
02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITestsLaunchTests.swift; sourceTree = "<group>"; };
02FFFD7A29395DD200306533 /* String+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Constants.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -51,6 +55,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4694AAA0293A7C8800D54903 /* Modules in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -71,13 +76,22 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
027F60592937662300467238 /* Login */ = {
isa = PBXGroup;
children = (
);
path = Login;
sourceTree = "<group>";
};
02AE64E229363DBF005A4AF3 = {
isa = PBXGroup;
children = (
02784F03293A8331005F839D /* Modules */,
02AE64ED29363DBF005A4AF3 /* BeReal */,
02AE64FE29363DC1005A4AF3 /* BeRealTests */,
02AE650829363DC1005A4AF3 /* BeRealUITests */,
02AE64EC29363DBF005A4AF3 /* Products */,
4694AA9E293A7C8800D54903 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -94,6 +108,8 @@
02AE64ED29363DBF005A4AF3 /* BeReal */ = {
isa = PBXGroup;
children = (
02FFFD7929395DBF00306533 /* Extensions */,
027F60592937662300467238 /* Login */,
02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */,
02AE64F029363DBF005A4AF3 /* ContentView.swift */,
02AE64F229363DC1005A4AF3 /* Assets.xcassets */,
@ -127,6 +143,21 @@
path = BeRealUITests;
sourceTree = "<group>";
};
02FFFD7929395DBF00306533 /* Extensions */ = {
isa = PBXGroup;
children = (
02FFFD7A29395DD200306533 /* String+Constants.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
4694AA9E293A7C8800D54903 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -143,6 +174,9 @@
dependencies = (
);
name = BeReal;
packageProductDependencies = (
4694AA9F293A7C8800D54903 /* Modules */,
);
productName = BeReal;
productReference = 02AE64EB29363DBF005A4AF3 /* BeReal.app */;
productType = "com.apple.product-type.application";
@ -260,6 +294,7 @@
files = (
02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */,
02AE64EF29363DBF005A4AF3 /* BeRealApp.swift in Sources */,
02FFFD7B29395DD200306533 /* String+Constants.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -608,6 +643,13 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
4694AA9F293A7C8800D54903 /* Modules */ = {
isa = XCSwiftPackageProductDependency;
productName = Modules;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 02AE64E329363DBF005A4AF3 /* Project object */;
}

View File

@ -7,6 +7,7 @@
//
import SwiftUI
import Login
struct ContentView: View {
var body: some View {
@ -17,6 +18,9 @@ struct ContentView: View {
Text("Hello, world!")
}
.padding()
.sheet(isPresented: .constant(true)) {
LoginView()
}
}
}

View File

@ -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 = ""
}

25
Modules/Package.swift Normal file
View File

@ -0,0 +1,25 @@
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "Modules",
defaultLocalization: "en",
platforms: [
.iOS(.v15)
],
products: [
.library(
name: "Modules",
targets: [
"Login"
]
),
],
targets: [
.target(
name: "Login",
resources: [.process("Resources")]
),
]
)

View File

@ -0,0 +1,12 @@
/*
Localizable.strings
Login
Created by Javier Cicchelli on 02/12/2022.
Copyright © 2022 Röck+Cöde. All rights reserved.
*/
"login.title.text" = "My files";
"login.text_field.username.placeholder" = "Username";
"login.text_field.password.placeholder" = "Password";
"login.button.log_in.text" = "Log in";

View File

@ -0,0 +1,154 @@
//
// LoginForm.swift
// Login
//
// Created by Javier Cicchelli on 01/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import SwiftUI
struct LoginForm: View {
// MARK: States
@FocusState private var focusedField: Field?
// MARK: Bindings
@Binding var username: String
@Binding var password: String
@Binding var errorMessage: String?
// MARK: Properties
let onReturn: () -> Void
// MARK: Body
var body: some View {
VStack(
alignment: .leading,
spacing: 16
) {
TextField(
NSLocalizedString(
"login.text_field.username.placeholder",
bundle: .module,
comment: "The placeholder for the username text field."
),
text: $username
) { isBeginEditing in
guard isBeginEditing, errorMessage != nil else { return }
errorMessage = nil
}
.textContentType(.username)
.lineLimit(1)
.autocapitalization(.none)
.disableAutocorrection(true)
.keyboardType(.default)
.focused($focusedField, equals: .username)
.onSubmit {
onUsernameReturnPressed()
}
Divider()
SecureField(
NSLocalizedString(
"login.text_field.password.placeholder",
bundle: .module,
comment: "The placeholder for the password text field."
),
text: $password
)
.textContentType(.password)
.lineLimit(1)
.autocapitalization(.none)
.disableAutocorrection(true)
.keyboardType(.default)
.focused($focusedField, equals: .password)
.onSubmit {
onPasswordReturnPressed()
}
if let errorMessage {
Divider()
Text(errorMessage)
.font(.body)
.foregroundColor(.red)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color.primary.colorInvert())
.cornerRadius(8)
.onAppear {
setClearButtonIfNeeded()
}
}
}
// MARK: - Helpers
private extension LoginForm {
func setClearButtonIfNeeded() {
let textFieldAppearance = UITextField.appearance()
guard textFieldAppearance.clearButtonMode != .whileEditing else { return }
textFieldAppearance.clearButtonMode = .whileEditing
}
func onUsernameReturnPressed() {
guard !username.isEmpty else { return }
if password.isEmpty {
focusedField = .password
} else {
onReturn()
}
}
func onPasswordReturnPressed() {
guard !password.isEmpty else { return }
if username.isEmpty {
focusedField = .username
} else {
onReturn()
}
}
}
// MARK: - Enumerations
private extension LoginForm {
enum Field: Hashable {
case username
case password
}
}
// MARK: - Previews
struct LoginForm_Previews: PreviewProvider {
static var previews: some View {
LoginForm(
username: .constant("Some username"),
password: .constant("Some Password"),
errorMessage: .constant(nil),
onReturn: {}
)
.previewDisplayName("Login form with no error message")
LoginForm(
username: .constant("Some username"),
password: .constant("Some Password"),
errorMessage: .constant("Some error goes in here..."),
onReturn: {}
)
.previewDisplayName("Login form with some error message")
}
}

View File

@ -0,0 +1,22 @@
//
// ViewHeightGeometry.swift
// Login
//
// Created by Javier Cicchelli on 02/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import SwiftUI
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
)
}
}
}

View File

@ -0,0 +1,17 @@
//
// ViewHeightPreferenceKey.swift
// Login
//
// Created by Javier Cicchelli on 02/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import SwiftUI
struct ViewHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}

View File

@ -0,0 +1,34 @@
//
// LogInLabelStyle.swift
// Login
//
// Created by Javier Cicchelli on 02/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import SwiftUI
struct LogInLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 8) {
Spacer()
configuration.title
.font(.body)
.foregroundColor(.primary)
configuration.icon
.tint(.primary)
Spacer()
}
}
}
// MARK: - LabelStyle
extension LabelStyle where Self == LogInLabelStyle {
static var logIn: Self {
LogInLabelStyle()
}
}

View File

@ -0,0 +1,117 @@
//
// LoginView.swift
// Login
//
// Created by Javier Cicchelli on 30/11/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import SwiftUI
public struct LoginView: View {
// MARK: States
@State private var containerTopPadding: CGFloat = 0
// MARK: Initialisers
public init() {}
// MARK: Body
public var body: some View {
ScrollView(
.vertical,
showsIndicators: false
) {
LoginContainer()
.padding(.horizontal, 24)
.padding(.top, containerTopPadding)
}
.background(Color.red)
.overlay(ViewHeightGeometry())
.onPreferenceChange(ViewHeightPreferenceKey.self) { height in
containerTopPadding = height * 0.1
}
}
}
// 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: Body
var body: some View {
VStack(spacing: 32) {
Text(
"login.title.text",
bundle: .module,
comment: "Login view title text."
)
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.primary)
LoginForm(
username: $username,
password: $password,
errorMessage: $errorMessage
) {
// TODO: login with the username and password.
}
Button {
// TODO: login with the username and password.
} label: {
Label {
Text(
"login.button.log_in.text",
bundle: .module,
comment: "Log in button text."
)
.fontWeight(.semibold)
} icon: {
if isAuthenticating {
ProgressView()
} else {
EmptyView()
}
}
.labelStyle(.logIn)
}
.tint(.orange)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 8))
.controlSize(.large)
.disabled(isLoginDisabled)
}
}
}
}
// MARK: - Helpers
private extension LoginView.LoginContainer {
var isLoginDisabled: Bool {
username.isEmpty || password.isEmpty
}
}
// MARK: - Previews
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}