be七365

Swift iOS登录界面开发实战教程:从零构建美观安全的登录页面并解决常见输入验证和网络请求问题

Swift iOS登录界面开发实战教程:从零构建美观安全的登录页面并解决常见输入验证和网络请求问题

引言:为什么需要一个优秀的登录界面

在移动应用开发中,登录界面是用户与应用交互的第一道门槛。一个设计精良、功能完善的登录界面不仅能提升用户体验,还能保障用户数据的安全。本教程将带你从零开始,使用Swift和UIKit构建一个既美观又安全的iOS登录界面,并深入探讨输入验证、网络请求处理等实际开发中的关键问题。

1. 项目准备与界面搭建

1.1 创建新项目

首先,打开Xcode,创建一个新的iOS项目:

选择”App”模板

语言设置为Swift

界面选择Storyboard(为了更直观地展示UI构建过程)

1.2 设计登录界面

在Storyboard中,我们拖拽以下UI组件构建登录表单:

// 在LoginViewController.swift中声明UI组件

class LoginViewController: UIViewController {

// 主容器视图

private let containerView: UIView = {

let view = UIView()

view.backgroundColor = .systemBackground

view.layer.cornerRadius = 12

view.layer.shadowColor = UIColor.label.cgColor

view.layer.shadowOpacity = 0.1

view.layer.shadowOffset = CGSize(width: 0, height: 2)

view.layer.shadowRadius = 4

view.translatesAutoresizingMaskIntoConstraints = false

return view

}()

// 标题标签

private let titleLabel: UILabel = {

let label = UILabel()

label.text = "欢迎回来"

label.font = UIFont.systemFont(ofSize: 24, weight: .bold)

label.textAlignment = .center

label.translatesAutoresizingMaskIntoConstraints = false

return label

}()

// 用户名输入框

private let usernameTextField: UITextField = {

let textField = UITextField()

textField.placeholder = "用户名/邮箱"

textField.borderStyle = .roundedRect

textField.autocapitalizationType = .none

textField.autocorrectionType = .no

textField.translatesAutoresizingMaskIntoConstraints = false

return textField

}()

// 密码输入框

private let passwordTextField: UITextField = {

let textField = UITextField()

textField.placeholder = "密码"

textField.borderStyle = .roundedRect

textField.isSecureTextEntry = true

textField.translatesAutoresizingMaskIntoConstraints = false

return textField

}()

// 登录按钮

private let loginButton: UIButton = {

let button = UIButton(type: .system)

button.setTitle("登录", for: .normal)

button.backgroundColor = .systemBlue

button.setTitleColor(.white, for: .normal)

button.layer.cornerRadius = 8

button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)

button.translatesAutoresizingMaskIntoConstraints = false

return button

}()

// 忘记密码按钮

private let forgotPasswordButton: UIButton = {

let button = UIButton(type: .system)

button.setTitle("忘记密码?", for: .normal)

button.setTitleColor(.systemGray, for: .normal)

button.titleLabel?.font = UIFont.systemFont(ofSize: 14)

button.translatesAutoresizingMaskIntoConstraints = false

return button

}()

// 注册按钮

private let registerButton: UIButton = {

let button = UIButton(type: .system)

let attributedTitle = NSAttributedString(

string: "没有账号? 立即注册",

attributes: [

.font: UIFont.systemFont(ofSize: 14),

.foregroundColor: UIColor.systemGray

]

)

button.setAttributedTitle(attributedTitle, for: .normal)

button.translatesAutoresizingMaskIntoConstraints = false

return button

}()

// 活动指示器

private let activityIndicator: UIActivityIndicatorView = {

let indicator = UIActivityIndicatorView(style: .medium)

indicator.color = .white

indicator.hidesWhenStopped = true

indicator.translatesAutoresizingMaskIntoConstraints = false

return indicator

}()

// 错误提示标签

private let errorLabel: UILabel = {

let label = UILabel()

label.textColor = .systemRed

label.font = UIFont.systemFont(ofSize: 12)

label.textAlignment = .center

label.numberOfLines = 0

label.isHidden = true

label.translatesAutoresizingMaskIntoConstraints = false

return label

}()

// 视图控制器生命周期

override func viewDidLoad() {

super.viewDidLoad()

setupUI()

setupConstraints()

setupActions()

setupTapGesture()

}

// 设置UI

private func setupUI() {

view.backgroundColor = .systemGroupedBackground

// 添加子视图

view.addSubview(containerView)

containerView.addSubview(titleLabel)

containerView.addSubview(usernameTextField)

containerView.addSubview(passwordTextField)

containerView.addSubview(loginButton)

containerView.addSubview(forgotPasswordButton)

containerView.addSubview(activityIndicator)

containerView.addSubview(errorLabel)

view.addSubview(registerButton)

}

// 设置约束

private func setupConstraints() {

NSLayoutConstraint.activate([

// 容器视图约束

containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),

containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50),

containerView.widthAnchor.constraint(equalToConstant: 300),

containerView.heightAnchor.constraint(equalToConstant: 350),

// 标题标签约束

titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20),

titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

titleLabel.heightAnchor.constraint(equalToConstant: 30),

// 用户名输入框约束

usernameTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),

usernameTextField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

usernameTextField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

usernameTextField.heightAnchor.constraint(equalToConstant: 40),

// 密码输入框约束

passwordTextField.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 15),

passwordTextField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

passwordTextField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

passwordTextField.heightAnchor.constraint(equalToConstant: 40),

// 登录按钮约束

loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 20),

loginButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

loginButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

loginButton.heightAnchor.constraint(equalToConstant: 44),

// 忘记密码按钮约束

forgotPasswordButton.topAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: 10),

forgotPasswordButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),

forgotPasswordButton.heightAnchor.constraint(equalToConstant: 20),

// 活动指示器约束

activityIndicator.centerXAnchor.constraint(equalTo: loginButton.centerXAnchor),

activityIndicator.centerYAnchor.constraint(equalTo: loginButton.centerYAnchor),

// 错误标签约束

errorLabel.topAnchor.constraint(equalTo: forgotPasswordButton.bottomAnchor, constant: 10),

errorLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

errorLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

errorLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),

// 注册按钮约束

registerButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),

registerButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),

registerButton.heightAnchor.constraint(equalToConstant: 20)

])

}

// 设置按钮动作

private func setupActions() {

loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)

forgotPasswordButton.addTarget(self, action: #selector(forgotPasswordTapped), for: .touchUpInside)

registerButton.addTarget(self, action: #selector(registerTapped), for: .touchUpInside)

}

// 设置点击手势(收起键盘)

private func setupTapGesture() {

let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))

view.addGestureRecognizer(tap)

}

// MARK: - Action Methods

@objc private func loginButtonTapped() {

// 后续实现

}

@objc private func forgotPasswordTapped() {

// 后续实现

}

@objc private func registerTapped() {

// 后续实现

}

@objc private func dismissKeyboard() {

view.endEditing(true)

}

}

1.3 界面优化与美化

为了提升用户体验,我们可以添加一些视觉优化:

// 在LoginViewController中添加以下方法

private func setupUIAppearance() {

// 设置渐变背景

let gradientLayer = CAGradientLayer()

gradientLayer.frame = view.bounds

gradientLayer.colors = [

UIColor.systemBlue.cgColor,

UIColor.systemPurple.cgColor

]

gradientLayer.startPoint = CGPoint(x: 0, y: 0)

gradientLayer.endPoint = CGPoint(x: 1, y: 1)

view.layer.insertSublayer(gradientLayer, at: 0)

// 设置输入框样式

[usernameTextField, passwordTextField].forEach { textField in

textField?.backgroundColor = .systemBackground.withAlphaComponent(0.9)

textField?.layer.cornerRadius = 8

textField?.layer.borderWidth = 1

textField?.layer.borderColor = UIColor.systemGray5.cgColor

textField?.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))

textField?.leftViewMode = .always

}

// 设置登录按钮样式

loginButton.layer.shadowColor = UIColor.systemBlue.cgColor

loginButton.layer.shadowOpacity = 0.3

loginButton.layer.shadowOffset = CGSize(width: 0, height: 2)

loginButton.layer.shadowRadius = 4

}

// 在viewDidLoad中调用

override func viewDidLoad() {

super.viewDidLoad()

setupUIAppearance() // 添加这行

setupUI()

setupConstraints()

setupActions()

setupTapGesture()

}

2. 输入验证逻辑实现

2.1 验证规则设计

在实际应用中,我们需要对用户输入进行严格验证:

// 创建一个验证工具类

struct InputValidator {

// 验证用户名

static func validateUsername(_ username: String) -> ValidationResult {

// 去除首尾空格

let trimmedUsername = username.trimmingCharacters(in: .whitespacesAndNewlines)

// 检查是否为空

if trimmedUsername.isEmpty {

return .failure("用户名不能为空")

}

// 检查长度

if trimmedUsername.count < 3 {

return .failure("用户名至少需要3个字符")

}

if trimmedUsername.count > 30 {

return .failure("用户名不能超过30个字符")

}

// 检查格式(允许字母、数字、下划线、连字符)

let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")

if trimmedUsername.rangeOfCharacter(from: allowedCharacters.inverted) != nil {

return .failure("用户名只能包含字母、数字、下划线和连字符")

}

return .success(trimmedUsername)

}

// 验证邮箱

static func validateEmail(_ email: String) -> ValidationResult {

let trimmedEmail = email.trimmingCharacters(in: .whitespacesAndNewlines)

if trimmedEmail.isEmpty {

return .failure("邮箱不能为空")

}

// 简单的邮箱格式验证

let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"

let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)

if !emailPredicate.evaluate(with: trimmedEmail) {

return .failure("请输入有效的邮箱地址")

}

return .success(trimmedEmail)

}

// 验证密码

static func validatePassword(_ password: String) -> ValidationResult {

let trimmedPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)

if trimmedPassword.isEmpty {

return .failure("密码不能为空")

}

if trimmedPassword.count < 8 {

return .failure("密码至少需要8个字符")

}

if trimmedPassword.count > 64 {

return .failure("密码不能超过64个字符")

}

// 检查密码强度(至少包含字母和数字)

let letterRegex = ".*[A-Za-z]+.*"

let numberRegex = ".*[0-9]+.*"

let letterPredicate = NSPredicate(format: "SELF MATCHES %@", letterRegex)

let numberPredicate = NSPredicate(format: "SELF MATCHES %@", numberRegex)

if !letterPredicate.evaluate(with: trimmedPassword) || !numberPredicate.evaluate(with: trimmedPassword) {

return .failure("密码必须包含字母和数字")

}

return .success(trimmedPassword)

}

// 验证结果枚举

enum ValidationResult {

case success(String)

case failure(String)

var isValid: Bool {

switch self {

case .success:

return true

case .failure:

return false

}

}

var value: String? {

switch self {

case .success(let value):

return value

case .failure:

return nil

}

}

var errorMessage: String? {

switch self {

case .success:

return nil

case .failure(let message):

return message

}

}

}

}

2.2 集成验证到登录流程

在登录按钮点击事件中集成验证:

@objc private func loginButtonTapped() {

// 收起键盘

dismissKeyboard()

// 隐藏之前的错误信息

errorLabel.isHidden = true

// 获取输入值

guard let username = usernameTextField.text,

let password = passwordTextField.text else {

showError("输入框不能为空")

return

}

// 验证用户名(可以是用户名或邮箱)

let usernameValidation: InputValidator.ValidationResult

if username.contains("@") {

usernameValidation = InputValidator.validateEmail(username)

} else {

usernameValidation = InputValidator.validateUsername(username)

}

guard usernameValidation.isValid else {

showError(usernameValidation.errorMessage ?? "用户名格式错误")

return

}

// 验证密码

let passwordValidation = InputValidator.validatePassword(password)

guard passwordValidation.isValid else {

showError(passwordValidation.errorMessage ?? "密码格式错误")

return

}

// 验证通过,开始登录流程

performLogin(with: usernameValidation.value!, password: passwordValidation.value!)

}

private func showError(_ message: String) {

errorLabel.text = message

errorLabel.isHidden = false

// 添加动画效果

UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: []) {

self.view.layoutIfNeeded()

}

}

3. 网络请求与安全处理

3.1 网络请求封装

使用URLSession封装一个安全的网络请求管理器:

import Foundation

// 网络错误枚举

enum NetworkError: Error, LocalizedError {

case invalidURL

case noData

case decodingError

case serverError(statusCode: Int)

case unauthorized

case networkUnavailable

case timeout

var errorDescription: String? {

switch self {

case .invalidURL:

return "无效的URL"

case .noData:

return "服务器未返回数据"

case .decodingError:

return "数据解析失败"

case .serverError(let statusCode):

return "服务器错误 (状态码: \(statusCode))"

case .unauthorized:

return "认证失败,请重新登录"

case .networkUnavailable:

return "网络不可用"

case .timeout:

return "请求超时"

}

}

}

// 网络请求管理器

class NetworkManager {

static let shared = NetworkManager()

private let session: URLSession

private let timeoutInterval: TimeInterval = 30

private init() {

let configuration = URLSessionConfiguration.default

configuration.timeoutIntervalForRequest = timeoutInterval

configuration.timeoutIntervalForResource = timeoutInterval

configuration.waitsForConnectivity = true

configuration.requestCachePolicy = .reloadIgnoringLocalCacheData

self.session = URLSession(configuration: configuration)

}

// 通用请求方法

func request(

endpoint: String,

method: String = "GET",

parameters: [String: Any]? = nil,

headers: [String: String]? = nil,

responseType: T.Type

) async throws -> T {

// 检查网络状态

guard await NetworkReachability.shared.isConnected else {

throw NetworkError.networkUnavailable

}

// 构建URL

guard let url = URL(string: endpoint) else {

throw NetworkError.invalidURL

}

// 构建请求

var request = URLRequest(url: url)

request.httpMethod = method

request.timeoutInterval = timeoutInterval

// 设置请求头

if let headers = headers {

for (key, value) in headers {

request.setValue(value, forHTTPHeaderField: key)

}

}

// 设置请求体

if let parameters = parameters, method != "GET" {

request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)

request.setValue("application/json", forHTTPHeaderField: "Content-Type")

}

// 发送请求

let (data, response) = try await session.data(for: request)

// 检查响应状态

guard let httpResponse = response as? HTTPURLResponse else {

throw NetworkError.serverError(statusCode: 0)

}

switch httpResponse.statusCode {

case 200...299:

// 成功响应,解析数据

do {

let decodedResponse = try JSONDecoder().decode(T.self, from: data)

return decodedResponse

} catch {

throw NetworkError.decodingError

}

case 401:

throw NetworkError.unauthorized

case 400...499:

throw NetworkError.serverError(statusCode: httpResponse.statusCode)

case 500...599:

throw NetworkError.serverError(statusCode: httpResponse.statusCode)

default:

throw NetworkError.serverError(statusCode: httpResponse.statusCode)

}

}

// 专门的登录请求方法

func login(username: String, password: String) async throws -> LoginResponse {

let parameters: [String: Any] = [

"username": username,

"password": password,

"device_info": getDeviceInfo()

]

let headers = [

"User-Agent": "MyApp/1.0 (iOS)",

"Accept": "application/json"

]

return try await request(

endpoint: "https://api.example.com/auth/login",

method: "POST",

parameters: parameters,

headers: headers,

responseType: LoginResponse.self

)

}

// 获取设备信息

private func getDeviceInfo() -> [String: String] {

return [

"platform": "iOS",

"device_model": UIDevice.current.model,

"system_version": UIDevice.current.systemVersion,

"app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"

]

}

}

// 响应模型

struct LoginResponse: Codable {

let success: Bool

let token: String?

let user: User?

let message: String?

}

struct User: Codable {

let id: String

let username: String

let email: String

let createdAt: String

enum CodingKeys: String, CodingKey {

case id, username, email

case createdAt = "created_at"

}

}

// 网络状态检查

class NetworkReachability {

static let shared = NetworkReachability()

private let monitor = NWPathMonitor()

private let queue = DispatchQueue(label: "NetworkReachability")

private init() {

monitor.pathUpdateHandler = { path in

self.isConnected = (path.status == .satisfied)

}

monitor.start(queue: queue)

}

private(set) var isConnected: Bool = true

}

3.2 在登录流程中集成网络请求

更新登录按钮点击事件:

@objc private func loginButtonTapped() {

// ... 前面的验证逻辑 ...

// 验证通过,开始登录流程

performLogin(with: usernameValidation.value!, password: passwordValidation.value!)

}

private func performLogin(with username: String, password: String) {

// 显示加载状态

setLoadingState(true)

// 使用async/await进行异步请求

Task {

do {

let loginResponse = try await NetworkManager.shared.login(username: username, password: password)

// 在主线程更新UI

await MainActor.run {

handleLoginResponse(response: loginResponse)

}

} catch let error as NetworkError {

await MainActor.run {

handleNetworkError(error)

}

} catch {

await MainActor.run {

showError("未知错误: \(error.localizedDescription)")

}

}

}

}

private func handleLoginResponse(response: LoginResponse) {

setLoadingState(false)

if response.success, let token = response.token {

// 登录成功,保存token

TokenManager.shared.saveToken(token)

// 跳转到主界面

navigateToMainScreen()

} else {

showError(response.message ?? "登录失败")

}

}

private func handleNetworkError(_ error: NetworkError) {

setLoadingState(false)

showError(error.localizedDescription)

}

private func setLoadingState(_ isLoading: Bool) {

if isLoading {

loginButton.setTitle("", for: .normal)

activityIndicator.startAnimating()

loginButton.isEnabled = false

} else {

activityIndicator.stopAnimating()

loginButton.setTitle("登录", for: .normal)

loginButton.isEnabled = true

}

}

private func navigateToMainScreen() {

// 创建主界面

let mainVC = MainViewController()

mainVC.modalPresentationStyle = .fullScreen

// 添加过渡动画

if let window = view.window {

UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve) {

window.rootViewController = mainVC

}

}

}

3.3 Token安全管理

创建Token管理器来安全存储认证信息:

import Security

class TokenManager {

static let shared = TokenManager()

private let tokenKey = "auth_token"

private let keychainService = "com.yourapp.auth"

private init() {}

// 保存token到Keychain

func saveToken(_ token: String) {

// 先删除旧的

deleteToken()

let data = token.data(using: .utf8)!

let query: [String: Any] = [

kSecClass as String: kSecClassGenericPassword,

kSecAttrService as String: keychainService,

kSecAttrAccount as String: tokenKey,

kSecValueData as String: data,

kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked

]

SecItemAdd(query as CFDictionary, nil)

}

// 从Keychain读取token

func getToken() -> String? {

let query: [String: Any] = [

kSecClass as String: kSecClassGenericPassword,

kSecAttrService as String: keychainService,

kSecAttrAccount as String: tokenKey,

kSecReturnData as String: true,

kSecMatchLimit as String: kSecMatchLimitOne

]

var result: AnyObject?

let status = SecItemCopyMatching(query as CFDictionary, &result)

guard status == errSecSuccess,

let data = result as? Data,

let token = String(data: data, encoding: .utf8) else {

return nil

}

return token

}

// 删除token

func deleteToken() {

let query: [String: Any] = [

kSecClass as String: kSecClassGenericPassword,

kSecAttrService as String: keychainService,

kSecAttrAccount as String: tokenKey

]

SecItemDelete(query as CFDictionary)

}

// 检查是否已登录

func isLoggedIn() -> Bool {

return getToken() != nil

}

}

4. 常见问题解决方案

4.1 处理键盘遮挡问题

当键盘弹出时,可能会遮挡输入框:

// 在LoginViewController中添加键盘通知处理

override func viewDidLoad() {

super.viewDidLoad()

// ... 其他设置 ...

setupKeyboardNotifications()

}

private func setupKeyboardNotifications() {

NotificationCenter.default.addObserver(

self,

selector: #selector(keyboardWillShow),

name: UIResponder.keyboardWillShowNotification,

object: nil

)

NotificationCenter.default.addObserver(

self,

selector: #selector(keyboardWillHide),

name: UIResponder.keyboardWillHideNotification,

object: nil

)

}

@objc private func keyboardWillShow(notification: NSNotification) {

guard let userInfo = notification.userInfo,

let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,

let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {

return

}

let keyboardHeight = keyboardFrame.height

// 调整容器视图的Y位置

if containerView.frame.origin.y == 0 {

UIView.animate(withDuration: duration) {

self.containerView.transform = CGAffineTransform(translationX: 0, y: -keyboardHeight / 2)

}

}

}

@objc private func keyboardWillHide(notification: NSNotification) {

guard let userInfo = notification.userInfo,

let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {

return

}

UIView.animate(withDuration: duration) {

self.containerView.transform = .identity

}

}

deinit {

NotificationCenter.default.removeObserver(self)

}

4.2 处理网络超时和重试

实现自动重试机制:

// 扩展NetworkManager添加重试功能

extension NetworkManager {

func requestWithRetry(

endpoint: String,

method: String = "GET",

parameters: [String: Any]? = nil,

headers: [String: String]? = nil,

responseType: T.Type,

maxRetries: Int = 3,

retryDelay: TimeInterval = 2.0

) async throws -> T {

var lastError: Error?

for attempt in 0...maxRetries {

do {

return try await request(

endpoint: endpoint,

method: method,

parameters: parameters,

headers: headers,

responseType: responseType

)

} catch {

lastError = error

// 如果是最后一次尝试,或者某些错误不应该重试

if attempt == maxRetries || shouldNotRetry(error: error) {

throw error

}

// 等待后重试

try await Task.sleep(nanoseconds: UInt64(retryDelay * 1_000_000_000))

}

}

throw lastError ?? NetworkError.networkUnavailable

}

private func shouldNotRetry(error: Error) -> Bool {

guard let networkError = error as? NetworkError else {

return false

}

// 这些错误不应该重试

switch networkError {

case .unauthorized, .invalidURL:

return true

default:

return false

}

}

}

// 在登录中使用带重试的请求

private func performLoginWithRetry(username: String, password: String) {

setLoadingState(true)

Task {

do {

let loginResponse = try await NetworkManager.shared.requestWithRetry(

endpoint: "https://api.example.com/auth/login",

method: "POST",

parameters: [

"username": username,

"password": password

],

headers: ["Accept": "application/json"],

responseType: LoginResponse.self,

maxRetries: 2

)

await MainActor.run {

handleLoginResponse(response: loginResponse)

}

} catch {

await MainActor.run {

handleNetworkError(error)

}

}

}

}

4.3 实现离线缓存策略

使用UserDefaults缓存最后的登录状态:

class OfflineCacheManager {

static let shared = OfflineCacheManager()

private let defaults = UserDefaults.standard

private let lastLoginKey = "last_login_info"

private init() {}

// 保存登录信息

func saveLastLogin(username: String, timestamp: Date = Date()) {

let info: [String: Any] = [

"username": username,

"timestamp": timestamp.timeIntervalSince1970

]

defaults.set(info, forKey: lastLoginKey)

}

// 获取最后登录信息

func getLastLogin() -> (username: String, timestamp: Date)? {

guard let info = defaults.dictionary(forKey: lastLoginKey),

let username = info["username"] as? String,

let timestamp = info["timestamp"] as? TimeInterval else {

return nil

}

// 检查是否过期(7天)

let date = Date(timeIntervalSince1970: timestamp)

if Date().timeIntervalSince(date) > 7 * 24 * 60 * 60 {

return nil

}

return (username, date)

}

// 清除缓存

func clearLastLogin() {

defaults.removeObject(forKey: lastLoginKey)

}

}

// 在登录成功后保存

private func handleLoginResponse(response: LoginResponse) {

if response.success, let token = response.token, let username = response.user?.username {

TokenManager.shared.saveToken(token)

OfflineCacheManager.shared.saveLastLogin(username: username)

navigateToMainScreen()

}

}

// 在登录界面加载时填充用户名

override func viewDidLoad() {

super.viewDidLoad()

// ... 其他设置 ...

// 如果有缓存的登录信息,填充用户名

if let lastLogin = OfflineCacheManager.shared.getLastLogin() {

usernameTextField.text = lastLogin.username

}

}

4.4 实现生物识别登录

集成Face ID/Touch ID:

import LocalAuthentication

class BiometricAuthManager {

static let shared = BiometricAuthManager()

private let context = LAContext()

private init() {}

// 检查设备是否支持生物识别

func isBiometricAvailable() -> Bool {

var error: NSError?

let isAvailable = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)

return isAvailable

}

// 获取生物识别类型

func getBiometricType() -> String {

var error: NSError?

context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)

if let error = error as? LAError {

if error.code == .biometryNotAvailable {

return "不可用"

}

}

if context.biometryType == .faceID {

return "Face ID"

} else if context.biometryType == .touchID {

return "Touch ID"

} else {

return "无"

}

}

// 执行生物识别验证

func authenticate(completion: @escaping (Bool, Error?) -> Void) {

let reason = "使用生物识别登录您的账户"

context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in

DispatchQueue.main.async {

completion(success, error)

}

}

}

}

// 在登录界面添加生物识别按钮

private let biometricLoginButton: UIButton = {

let button = UIButton(type: .system)

button.setTitle("使用Face ID登录", for: .normal)

button.backgroundColor = .systemGray6

button.setTitleColor(.label, for: .normal)

button.layer.cornerRadius = 8

button.translatesAutoresizingMaskIntoConstraints = false

button.isHidden = true // 默认隐藏

return button

}()

// 在viewDidLoad中配置

private func setupBiometricButton() {

if BiometricAuthManager.shared.isBiometricAvailable() {

biometricLoginButton.isHidden = false

biometricLoginButton.setTitle(

"使用\(BiometricAuthManager.shared.getBiometricType())登录",

for: .normal

)

biometricLoginButton.addTarget(self, action: #selector(biometricLoginTapped), for: .touchUpInside)

}

}

@objc private func biometricLoginTapped() {

BiometricAuthManager.shared.authenticate { [weak self] success, error in

if success {

// 从Keychain获取token并自动登录

if let token = TokenManager.shared.getToken() {

self?.navigateToMainScreen()

} else {

self?.showError("请先进行一次密码登录")

}

} else {

self?.showError(error?.localizedDescription ?? "生物识别验证失败")

}

}

}

5. 完整的登录流程整合

5.1 完整的LoginViewController

将所有功能整合到一个完整的视图控制器中:

import UIKit

import LocalAuthentication

import Network

class LoginViewController: UIViewController {

// MARK: - UI Components

private let backgroundImage: UIImageView = {

let imageView = UIImageView(image: UIImage(systemName: "waveform.path.ecg"))

imageView.contentMode = .scaleAspectFill

imageView.alpha = 0.1

imageView.translatesAutoresizingMaskIntoConstraints = false

return imageView

}()

private let containerView: UIView = {

let view = UIView()

view.backgroundColor = .systemBackground

view.layer.cornerRadius = 16

view.layer.shadowColor = UIColor.label.cgColor

view.layer.shadowOpacity = 0.1

view.layer.shadowOffset = CGSize(width: 0, height: 4)

view.layer.shadowRadius = 8

view.translatesAutoresizingMaskIntoConstraints = false

return view

}()

private let titleLabel: UILabel = {

let label = UILabel()

label.text = "欢迎回来"

label.font = UIFont.systemFont(ofSize: 28, weight: .bold)

label.textAlignment = .center

label.textColor = .label

label.translatesAutoresizingMaskIntoConstraints = false

return label

}()

private let subtitleLabel: UILabel = {

let label = UILabel()

label.text = "请登录您的账户"

label.font = UIFont.systemFont(ofSize: 14, weight: .regular)

label.textAlignment = .center

label.textColor = .secondaryLabel

label.translatesAutoresizingMaskIntoConstraints = false

return label

}()

private let usernameTextField: UITextField = {

let textField = UITextField()

textField.placeholder = "用户名或邮箱"

textField.borderStyle = .roundedRect

textField.autocapitalizationType = .none

textField.autocorrectionType = .no

textField.keyboardType = .emailAddress

textField.translatesAutoresizingMaskIntoConstraints = false

return textField

}()

private let passwordTextField: UITextField = {

let textField = UITextField()

textField.placeholder = "密码"

textField.borderStyle = .roundedRect

textField.isSecureTextEntry = true

textField.translatesAutoresizingMaskIntoConstraints = false

return textField

}()

private let loginButton: UIButton = {

let button = UIButton(type: .system)

button.setTitle("登录", for: .normal)

button.backgroundColor = .systemBlue

button.setTitleColor(.white, for: .normal)

button.layer.cornerRadius = 10

button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)

button.translatesAutoresizingMaskIntoConstraints = false

return button

}()

private let biometricLoginButton: UIButton = {

let button = UIButton(type: .system)

button.backgroundColor = .systemGray6

button.setTitleColor(.label, for: .normal)

button.layer.cornerRadius = 10

button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)

button.translatesAutoresizingMaskIntoConstraints = false

button.isHidden = true

return button

}()

private let forgotPasswordButton: UIButton = {

let button = UIButton(type: .system)

button.setTitle("忘记密码?", for: .normal)

button.setTitleColor(.systemGray, for: .normal)

button.titleLabel?.font = UIFont.systemFont(ofSize: 14)

button.translatesAutoresizingMaskIntoConstraints = false

return button

}()

private let registerButton: UIButton = {

let button = UIButton(type: .system)

let attributedTitle = NSAttributedString(

string: "没有账号? 立即注册",

attributes: [

.font: UIFont.systemFont(ofSize: 14, weight: .medium),

.foregroundColor: UIColor.systemBlue

]

)

button.setAttributedTitle(attributedTitle, for: .normal)

button.translatesAutoresizingMaskIntoConstraints = false

return button

}()

private let activityIndicator: UIActivityIndicatorView = {

let indicator = UIActivityIndicatorView(style: .medium)

indicator.color = .white

indicator.hidesWhenStopped = true

indicator.translatesAutoresizingMaskIntoConstraints = false

return indicator

}()

private let errorLabel: UILabel = {

let label = UILabel()

label.textColor = .systemRed

label.font = UIFont.systemFont(ofSize: 12, weight: .medium)

label.textAlignment = .center

label.numberOfLines = 0

label.isHidden = true

label.translatesAutoresizingMaskIntoConstraints = false

return label

}()

// MARK: - Properties

private var isLoggingIn = false

// MARK: - Lifecycle

override func viewDidLoad() {

super.viewDidLoad()

setupUI()

setupConstraints()

setupActions()

setupTapGesture()

setupKeyboardNotifications()

setupBiometricButton()

checkNetworkStatus()

}

override func viewDidAppear(_ animated: Bool) {

super.viewDidAppear(animated)

// 自动聚焦到用户名输入框

usernameTextField.becomeFirstResponder()

}

// MARK: - Setup Methods

private func setupUI() {

view.backgroundColor = .systemGroupedBackground

// 添加背景图片

view.addSubview(backgroundImage)

// 添加容器视图

view.addSubview(containerView)

containerView.addSubview(titleLabel)

containerView.addSubview(subtitleLabel)

containerView.addSubview(usernameTextField)

containerView.addSubview(passwordTextField)

containerView.addSubview(loginButton)

containerView.addSubview(biometricLoginButton)

containerView.addSubview(forgotPasswordButton)

containerView.addSubview(activityIndicator)

containerView.addSubview(errorLabel)

view.addSubview(registerButton)

// 设置输入框样式

[usernameTextField, passwordTextField].forEach { textField in

textField?.backgroundColor = .systemBackground

textField?.layer.cornerRadius = 8

textField?.layer.borderWidth = 1

textField?.layer.borderColor = UIColor.systemGray5.cgColor

textField?.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 0))

textField?.leftViewMode = .always

}

}

private func setupConstraints() {

NSLayoutConstraint.activate([

// 背景图片

backgroundImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),

backgroundImage.centerYAnchor.constraint(equalTo: view.centerYAnchor),

backgroundImage.widthAnchor.constraint(equalToConstant: 200),

backgroundImage.heightAnchor.constraint(equalToConstant: 200),

// 容器视图

containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),

containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor),

containerView.widthAnchor.constraint(equalToConstant: 320),

containerView.heightAnchor.constraint(equalToConstant: 420),

// 标题

titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 24),

titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

// 副标题

subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),

subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

subtitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

// 用户名输入框

usernameTextField.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 24),

usernameTextField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

usernameTextField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

usernameTextField.heightAnchor.constraint(equalToConstant: 44),

// 密码输入框

passwordTextField.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 12),

passwordTextField.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

passwordTextField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

passwordTextField.heightAnchor.constraint(equalToConstant: 44),

// 登录按钮

loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 20),

loginButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

loginButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

loginButton.heightAnchor.constraint(equalToConstant: 44),

// 生物识别按钮

biometricLoginButton.topAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: 8),

biometricLoginButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

biometricLoginButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

biometricLoginButton.heightAnchor.constraint(equalToConstant: 44),

// 忘记密码按钮

forgotPasswordButton.topAnchor.constraint(equalTo: biometricLoginButton.bottomAnchor, constant: 12),

forgotPasswordButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),

forgotPasswordButton.heightAnchor.constraint(equalToConstant: 20),

// 活动指示器

activityIndicator.centerXAnchor.constraint(equalTo: loginButton.centerXAnchor),

activityIndicator.centerYAnchor.constraint(equalTo: loginButton.centerYAnchor),

// 错误标签

errorLabel.topAnchor.constraint(equalTo: forgotPasswordButton.bottomAnchor, constant: 8),

errorLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),

errorLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),

errorLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),

// 注册按钮

registerButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),

registerButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),

registerButton.heightAnchor.constraint(equalToConstant: 20)

])

}

private func setupActions() {

loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)

biometricLoginButton.addTarget(self, action: #selector(biometricLoginTapped), for: .touchUpInside)

forgotPasswordButton.addTarget(self, action: #selector(forgotPasswordTapped), for: .touchUpInside)

registerButton.addTarget(self, action: #selector(registerTapped), for: .touchUpInside)

}

private func setupTapGesture() {

let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))

tap.cancelsTouchesInView = false

view.addGestureRecognizer(tap)

}

private func setupKeyboardNotifications() {

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)

}

private func setupBiometricButton() {

if BiometricAuthManager.shared.isBiometricAvailable() {

biometricLoginButton.isHidden = false

let biometricType = BiometricAuthManager.shared.getBiometricType()

biometricLoginButton.setTitle("使用\(biometricType)登录", for: .normal)

}

}

private func checkNetworkStatus() {

let monitor = NWPathMonitor()

monitor.pathUpdateHandler = { [weak self] path in

DispatchQueue.main.async {

if path.status != .satisfied {

self?.showError("网络不可用,请检查网络连接")

}

}

}

monitor.start(queue: DispatchQueue(label: "NetworkMonitor"))

}

// MARK: - Action Methods

@objc private func loginButtonTapped() {

performLogin()

}

@objc private func biometricLoginTapped() {

BiometricAuthManager.shared.authenticate { [weak self] success, error in

if success {

self?.handleBiometricLogin()

} else {

self?.showError(error?.localizedDescription ?? "生物识别验证失败")

}

}

}

@objc private func forgotPasswordTapped() {

// 跳转到忘记密码界面

let forgotVC = ForgotPasswordViewController()

present(forgotVC, animated: true)

}

@objc private func registerTapped() {

// 跳转到注册界面

let registerVC = RegisterViewController()

present(registerVC, animated: true)

}

@objc private func dismissKeyboard() {

view.endEditing(true)

}

// MARK: - Login Logic

private func performLogin() {

guard !isLoggingIn else { return }

dismissKeyboard()

errorLabel.isHidden = true

guard let username = usernameTextField.text,

let password = passwordTextField.text else {

showError("输入框不能为空")

return

}

// 验证输入

let usernameValidation: InputValidator.ValidationResult

if username.contains("@") {

usernameValidation = InputValidator.validateEmail(username)

} else {

usernameValidation = InputValidator.validateUsername(username)

}

guard usernameValidation.isValid else {

showError(usernameValidation.errorMessage ?? "用户名格式错误")

return

}

let passwordValidation = InputValidator.validatePassword(password)

guard passwordValidation.isValid else {

showError(passwordValidation.errorMessage ?? "密码格式错误")

return

}

// 执行登录

executeLoginRequest(

username: usernameValidation.value!,

password: passwordValidation.value!

)

}

private func executeLoginRequest(username: String, password: String) {

isLoggingIn = true

setLoadingState(true)

Task {

do {

let response = try await NetworkManager.shared.requestWithRetry(

endpoint: "https://api.example.com/auth/login",

method: "POST",

parameters: [

"username": username,

"password": password,

"device_info": getDeviceInfo()

],

headers: [

"User-Agent": "MyApp/1.0 (iOS)",

"Accept": "application/json"

],

responseType: LoginResponse.self,

maxRetries: 2

)

await MainActor.run {

handleLoginResponse(response, username: username)

}

} catch let error as NetworkError {

await MainActor.run {

handleNetworkError(error)

}

} catch {

await MainActor.run {

showError("未知错误: \(error.localizedDescription)")

}

}

}

}

private func handleLoginResponse(_ response: LoginResponse, username: String) {

isLoggingIn = false

setLoadingState(false)

if response.success, let token = response.token {

TokenManager.shared.saveToken(token)

OfflineCacheManager.shared.saveLastLogin(username: username)

if let user = response.user {

// 保存用户信息

saveUserInfo(user)

}

navigateToMainScreen()

} else {

showError(response.message ?? "登录失败")

}

}

private func handleBiometricLogin() {

// 从Keychain获取token

guard let token = TokenManager.shared.getToken() else {

showError("请先进行一次密码登录")

return

}

// 验证token是否有效(可选)

validateToken(token) { [weak self] isValid in

if isValid {

self?.navigateToMainScreen()

} else {

self?.showError("登录已过期,请重新登录")

}

}

}

private func validateToken(_ token: String, completion: @escaping (Bool) -> Void) {

// 这里可以调用一个简单的API来验证token

// 为了演示,我们假设token有效

completion(true)

}

private func handleNetworkError(_ error: NetworkError) {

isLoggingIn = false

setLoadingState(false)

switch error {

case .networkUnavailable:

showError("网络不可用,请检查网络连接")

case .timeout:

showError("请求超时,请稍后重试")

case .unauthorized:

showError("用户名或密码错误")

case .serverError(let code):

showError("服务器错误 (代码: \(code))")

default:

showError(error.localizedDescription)

}

}

// MARK: - UI Updates

private func setLoadingState(_ isLoading: Bool) {

if isLoading {

loginButton.setTitle("", for: .normal)

activityIndicator.startAnimating()

loginButton.isEnabled = false

biometricLoginButton.isEnabled = false

usernameTextField.isEnabled = false

passwordTextField.isEnabled = false

} else {

activityIndicator.stopAnimating()

loginButton.setTitle("登录", for: .normal)

loginButton.isEnabled = true

biometricLoginButton.isEnabled = true

usernameTextField.isEnabled = true

passwordTextField.isEnabled = true

}

}

private func showError(_ message: String) {

errorLabel.text = message

errorLabel.isHidden = false

// 动画效果

errorLabel.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)

UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {

self.errorLabel.transform = .identity

self.view.layoutIfNeeded()

}

}

// MARK: - Navigation

private func navigateToMainScreen() {

let mainVC = MainViewController()

mainVC.modalPresentationStyle = .fullScreen

if let window = view.window {

UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve) {

window.rootViewController = mainVC

}

}

}

// MARK: - Keyboard Handling

@objc private func keyboardWillShow(notification: NSNotification) {

guard let userInfo = notification.userInfo,

let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,

let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {

return

}

let keyboardHeight = keyboardFrame.height

// 只在容器视图被遮挡时调整

let containerBottom = containerView.frame.maxY

let viewHeight = view.frame.height

let safeAreaBottom = view.safeAreaInsets.bottom

if containerBottom > viewHeight - keyboardHeight - safeAreaBottom {

UIView.animate(withDuration: duration) {

self.containerView.transform = CGAffineTransform(translationX: 0, y: -(keyboardHeight / 2))

}

}

}

@objc private func keyboardWillHide(notification: NSNotification) {

guard let userInfo = notification.userInfo,

let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else {

return

}

UIView.animate(withDuration: duration) {

self.containerView.transform = .identity

}

}

// MARK: - Helper Methods

private func getDeviceInfo() -> [String: String] {

return [

"platform": "iOS",

"device_model": UIDevice.current.model,

"system_version": UIDevice.current.systemVersion,

"app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"

]

}

private func saveUserInfo(_ user: User) {

let userInfo: [String: String] = [

"id": user.id,

"username": user.username,

"email": user.email,

"createdAt": user.createdAt

]

UserDefaults.standard.set(userInfo, forKey: "current_user")

}

deinit {

NotificationCenter.default.removeObserver(self)

}

}

5.2 辅助视图控制器

为了完整性,提供简单的辅助视图:

// 忘记密码视图控制器

class ForgotPasswordViewController: UIViewController {

override func viewDidLoad() {

super.viewDidLoad()

view.backgroundColor = .systemBackground

let label = UILabel()

label.text = "忘记密码功能"

label.textAlignment = .center

label.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(label)

NSLayoutConstraint.activate([

label.centerXAnchor.constraint(equalTo: view.centerXAnchor),

label.centerYAnchor.constraint(equalTo: view.centerYAnchor)

])

}

}

// 注册视图控制器

class RegisterViewController: UIViewController {

override func viewDidLoad() {

super.viewDidLoad()

view.backgroundColor = .systemBackground

let label = UILabel()

label.text = "注册功能"

label.textAlignment = .center

label.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(label)

NSLayoutConstraint.activate([

label.centerXAnchor.constraint(equalTo: view.centerXAnchor),

label.centerYAnchor.constraint(equalTo: view.centerYAnchor)

])

}

}

// 主界面视图控制器

class MainViewController: UIViewController {

override func viewDidLoad() {

super.viewDidLoad()

view.backgroundColor = .systemBackground

let label = UILabel()

label.text = "登录成功!这是主界面"

label.textAlignment = .center

label.font = UIFont.systemFont(ofSize: 24, weight: .bold)

label.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(label)

NSLayoutConstraint.activate([

label.centerXAnchor.constraint(equalTo: view.centerXAnchor),

label.centerYAnchor.constraint(equalTo: view.centerYAnchor)

])

}

}

6. 测试与优化建议

6.1 单元测试示例

为验证逻辑编写测试:

import XCTest

@testable import YourApp

class InputValidatorTests: XCTestCase {

func testValidUsername() {

let result = InputValidator.validateUsername("user123")

XCTAssertTrue(result.isValid)

XCTAssertEqual(result.value, "user123")

}

func testInvalidUsernameTooShort() {

let result = InputValidator.validateUsername("ab")

XCTAssertFalse(result.isValid)

XCTAssertEqual(result.errorMessage, "用户名至少需要3个字符")

}

func testValidEmail() {

let result = InputValidator.validateEmail("test@example.com")

XCTAssertTrue(result.isValid)

}

func testInvalidEmail() {

let result = InputValidator.validateEmail("invalid-email")

XCTAssertFalse(result.isValid)

}

func testValidPassword() {

let result = InputValidator.validatePassword("password123")

XCTAssertTrue(result.isValid)

}

func testInvalidPasswordTooShort() {

let result = InputValidator.validatePassword("pass1")

XCTAssertFalse(result.isValid)

XCTAssertEqual(result.errorMessage, "密码至少需要8个字符")

}

}

6.2 性能优化建议

图片优化: 使用SF Symbols代替自定义图片

内存管理: 使用weak self避免循环引用

网络优化: 实现请求缓存,减少不必要的网络调用

UI渲染: 使用shouldRasterize优化复杂视图层次

6.3 安全最佳实践

始终使用HTTPS: 确保所有API调用使用SSL/TLS

输入清理: 在发送到服务器前清理所有输入

错误信息: 不要向用户暴露过多技术细节

速率限制: 在服务器端实现登录尝试限制

Token刷新: 实现自动token刷新机制

7. 总结

本教程详细介绍了如何使用Swift构建一个完整的iOS登录界面,涵盖了从UI设计到安全实现的各个方面。关键要点包括:

美观的UI: 使用Auto Layout和现代UI设计原则

严格的验证: 客户端和服务器端双重验证

安全的网络请求: 使用Keychain存储token,实现重试机制

用户体验: 处理键盘遮挡、加载状态、错误提示

高级功能: 生物识别登录、离线缓存、网络状态检查

通过这些实践,你可以创建一个既安全又用户友好的登录界面,为你的应用打下坚实的基础。记住,安全是一个持续的过程,需要定期更新和审查你的实现。

相关推荐