Product Components/LoginScreen
Screen
LoginScreen
A complete authentication screen composed entirely from SwiftDS base components. Includes email/password fields, a primary action button, social login divider, and a link to sign up. Handles loading, error, and success states with zero custom styling.
Preview
Welcome back
Sign in to your account
you@example.com
Password
••••••••
Sign in
or
Continue with Google
Don't have an account? Sign up
Components used
DSTextField
Email field with leading icon
DSSecureField
Password with show/hide toggle
DSButton
.primary for Sign in
DSButton
.secondary for social login
DSDivider
Or divider between methods
DSSpinner
Loading state while authenticating
Full implementation
LoginScreen.swift
import SwiftUI
import SwiftDS
struct LoginScreen: View {
@StateObject private var vm = LoginViewModel()
var body: some View {
DSPageLayout {
VStack(spacing: DSSpacing.xxl) {
// Logo & headline
VStack(spacing: DSSpacing.sm) {
DSAppIcon()
DSText("Welcome back", style: .title2)
DSText("Sign in to your account", style: .subheadline)
.foregroundColor(DSColor.textSecondary)
}
// Form
VStack(spacing: DSSpacing.md) {
DSTextField(
label: "Email",
placeholder: "you@example.com",
text: $vm.email,
leadingIcon: "envelope",
error: vm.emailError
)
DSSecureField(
label: "Password",
placeholder: "••••••••",
text: $vm.password,
error: vm.passwordError
)
HStack {
Spacer()
Button("Forgot password?") { vm.forgotPassword() }
.font(.system(size: 13))
.foregroundColor(DSColor.primary)
}
}
// Actions
VStack(spacing: DSSpacing.sm) {
DSButton(
"Sign in",
variant: .primary,
isLoading: vm.isLoading,
action: { vm.login() }
)
DSSection {
DSDivider(label: "or")
}
DSButton(
"Continue with Google",
variant: .secondary,
icon: "globe",
action: { vm.loginWithGoogle() }
)
}
// Sign up link
HStack(spacing: 4) {
DSText("Don't have an account?", style: .footnote)
.foregroundColor(DSColor.textSecondary)
Button("Sign up") { vm.navigateToSignUp() }
.font(.system(size: 13, weight: .semibold))
.foregroundColor(DSColor.primary)
}
}
.padding(DSSpacing.xl)
}
.dsToast(
isPresented: $vm.showError,
title: "Login failed",
message: vm.errorMessage,
style: .error
)
}
}ViewModel pattern
LoginViewModel.swift
@MainActor
class LoginViewModel: ObservableObject {
@Published var email = ""
@Published var password = ""
@Published var isLoading = false
@Published var showError = false
@Published var emailError: String?
@Published var passwordError: String?
@Published var errorMessage = ""
func login() {
guard validate() else { return }
isLoading = true
Task {
do {
try await AuthService.shared.login(email: email, password: password)
} catch {
errorMessage = error.localizedDescription
showError = true
}
isLoading = false
}
}
private func validate() -> Bool {
emailError = email.isEmpty ? "Email is required" : nil
passwordError = password.count < 6 ? "Password must be at least 6 characters" : nil
return emailError == nil && passwordError == nil
}
}