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

Email

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
    }
}