|
|
import SwiftUI
|
|
|
import Foundation
|
|
|
|
|
|
// MARK: - 通用工具函数
|
|
|
|
|
|
// MARK: - 字符串扩展
|
|
|
extension String {
|
|
|
/// 检查字符串是否为有效的邮箱地址
|
|
|
var isValidEmail: Bool {
|
|
|
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
|
|
|
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
|
|
|
return emailPredicate.evaluate(with: self)
|
|
|
}
|
|
|
|
|
|
/// 检查字符串是否为有效的电话号码
|
|
|
var isValidPhone: Bool {
|
|
|
let phoneRegex = "^[+]?[0-9\\s\\-\\(\\)]{7,}$"
|
|
|
let phonePredicate = NSPredicate(format: "SELF MATCHES %@", phoneRegex)
|
|
|
return phonePredicate.evaluate(with: self)
|
|
|
}
|
|
|
|
|
|
/// 检查字符串是否为有效的URL
|
|
|
var isValidURL: Bool {
|
|
|
guard let url = URL(string: self) else { return false }
|
|
|
return UIApplication.shared.canOpenURL(url)
|
|
|
}
|
|
|
|
|
|
/// 移除字符串两端的空白字符
|
|
|
var trimmed: String {
|
|
|
self.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
}
|
|
|
|
|
|
/// 检查字符串是否为空或只包含空白字符
|
|
|
var isEmptyOrWhitespace: Bool {
|
|
|
self.trimmed.isEmpty
|
|
|
}
|
|
|
|
|
|
/// 获取字符串的字符数(不包括空白字符)
|
|
|
var characterCount: Int {
|
|
|
self.trimmed.count
|
|
|
}
|
|
|
|
|
|
/// 截取字符串到指定长度
|
|
|
func truncated(to length: Int, suffix: String = "...") -> String {
|
|
|
if self.count <= length {
|
|
|
return self
|
|
|
}
|
|
|
let index = self.index(self.startIndex, offsetBy: length - suffix.count)
|
|
|
return String(self[..<index]) + suffix
|
|
|
}
|
|
|
|
|
|
/// 添加URL协议前缀
|
|
|
func withURLProtocol() -> String {
|
|
|
if self.hasPrefix("http://") || self.hasPrefix("https://") {
|
|
|
return self
|
|
|
}
|
|
|
return "https://" + self
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 日期扩展
|
|
|
extension Date {
|
|
|
/// 格式化日期为字符串
|
|
|
func formattedString(style: DateFormatter.Style = .medium) -> String {
|
|
|
let formatter = DateFormatter()
|
|
|
formatter.dateStyle = style
|
|
|
formatter.locale = Locale(identifier: "zh_CN")
|
|
|
return formatter.string(from: self)
|
|
|
}
|
|
|
|
|
|
/// 格式化日期为时间字符串
|
|
|
func formattedTimeString() -> String {
|
|
|
let formatter = DateFormatter()
|
|
|
formatter.timeStyle = .short
|
|
|
formatter.locale = Locale(identifier: "zh_CN")
|
|
|
return formatter.string(from: self)
|
|
|
}
|
|
|
|
|
|
/// 格式化日期为完整字符串
|
|
|
func formattedFullString() -> String {
|
|
|
let formatter = DateFormatter()
|
|
|
formatter.dateStyle = .medium
|
|
|
formatter.timeStyle = .short
|
|
|
formatter.locale = Locale(identifier: "zh_CN")
|
|
|
return formatter.string(from: self)
|
|
|
}
|
|
|
|
|
|
/// 检查日期是否为今天
|
|
|
var isToday: Bool {
|
|
|
Calendar.current.isDateInToday(self)
|
|
|
}
|
|
|
|
|
|
/// 检查日期是否为昨天
|
|
|
var isYesterday: Bool {
|
|
|
Calendar.current.isDateInYesterday(self)
|
|
|
}
|
|
|
|
|
|
/// 获取相对时间描述
|
|
|
var relativeTimeDescription: String {
|
|
|
let now = Date()
|
|
|
let components = Calendar.current.dateComponents([.minute, .hour, .day], from: self, to: now)
|
|
|
|
|
|
if let day = components.day, day > 0 {
|
|
|
if day == 1 {
|
|
|
return "yesterday".localized
|
|
|
} else if day < 7 {
|
|
|
return String(format: "days_ago".localized, day)
|
|
|
} else {
|
|
|
return self.formattedString(style: .short)
|
|
|
}
|
|
|
} else if let hour = components.hour, hour > 0 {
|
|
|
return String(format: "hours_ago".localized, hour)
|
|
|
} else if let minute = components.minute, minute > 0 {
|
|
|
return String(format: "minutes_ago".localized, minute)
|
|
|
} else {
|
|
|
return "just_now".localized
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 颜色扩展
|
|
|
extension Color {
|
|
|
/// 创建随机颜色
|
|
|
static func random() -> Color {
|
|
|
Color(
|
|
|
red: Double.random(in: 0...1),
|
|
|
green: Double.random(in: 0...1),
|
|
|
blue: Double.random(in: 0...1)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
/// 创建系统颜色
|
|
|
static let systemBackground = Color(.systemBackground)
|
|
|
static let systemGroupedBackground = Color(.systemGroupedBackground)
|
|
|
|
|
|
/// 创建语义颜色
|
|
|
static let label = Color(.label)
|
|
|
static let secondaryLabel = Color(.secondaryLabel)
|
|
|
static let tertiaryLabel = Color(.tertiaryLabel)
|
|
|
static let quaternaryLabel = Color(.quaternaryLabel)
|
|
|
|
|
|
/// 创建系统颜色
|
|
|
static let systemBlue = Color(.systemBlue)
|
|
|
static let systemGreen = Color(.systemGreen)
|
|
|
static let systemIndigo = Color(.systemIndigo)
|
|
|
static let systemOrange = Color(.systemOrange)
|
|
|
static let systemPink = Color(.systemPink)
|
|
|
static let systemPurple = Color(.systemPurple)
|
|
|
static let systemRed = Color(.systemRed)
|
|
|
static let systemTeal = Color(.systemTeal)
|
|
|
static let systemYellow = Color(.systemYellow)
|
|
|
|
|
|
/// 创建系统灰色
|
|
|
static let systemGray = Color(.systemGray)
|
|
|
static let systemGray2 = Color(.systemGray2)
|
|
|
static let systemGray3 = Color(.systemGray3)
|
|
|
static let systemGray4 = Color(.systemGray4)
|
|
|
static let systemGray5 = Color(.systemGray5)
|
|
|
static let systemGray6 = Color(.systemGray6)
|
|
|
}
|
|
|
|
|
|
// MARK: - 视图扩展
|
|
|
extension View {
|
|
|
/// 添加圆角
|
|
|
func roundedCorners(_ radius: CGFloat, corners: UIRectCorner = .allCorners) -> some View {
|
|
|
clipShape(RoundedCorner(radius: radius, corners: corners))
|
|
|
}
|
|
|
|
|
|
/// 添加自定义阴影
|
|
|
func customShadow(
|
|
|
color: Color = .black.opacity(0.1),
|
|
|
radius: CGFloat = 8,
|
|
|
x: CGFloat = 0,
|
|
|
y: CGFloat = 4
|
|
|
) -> some View {
|
|
|
self.shadow(color: color, radius: radius, x: x, y: y)
|
|
|
}
|
|
|
|
|
|
/// 添加边框
|
|
|
func customBorder(
|
|
|
_ color: Color,
|
|
|
width: CGFloat = 1,
|
|
|
cornerRadius: CGFloat = 0
|
|
|
) -> some View {
|
|
|
self.overlay(
|
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
|
.stroke(color, lineWidth: width)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
/// 添加自定义背景色
|
|
|
func customBackground(_ color: Color, cornerRadius: CGFloat = 0) -> some View {
|
|
|
self.background(
|
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
|
.fill(color)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
/// 隐藏键盘
|
|
|
func hideKeyboard() {
|
|
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 自定义圆角形状
|
|
|
struct RoundedCorner: Shape {
|
|
|
var radius: CGFloat = .infinity
|
|
|
var corners: UIRectCorner = .allCorners
|
|
|
|
|
|
func path(in rect: CGRect) -> Path {
|
|
|
let path = UIBezierPath(
|
|
|
roundedRect: rect,
|
|
|
byRoundingCorners: corners,
|
|
|
cornerRadii: CGSize(width: radius, height: radius)
|
|
|
)
|
|
|
return Path(path.cgPath)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 通用验证函数
|
|
|
struct ValidationHelper {
|
|
|
/// 验证邮箱地址
|
|
|
static func isValidEmail(_ email: String) -> Bool {
|
|
|
email.isValidEmail
|
|
|
}
|
|
|
|
|
|
/// 验证电话号码
|
|
|
static func isValidPhone(_ phone: String) -> Bool {
|
|
|
phone.isValidPhone
|
|
|
}
|
|
|
|
|
|
/// 验证URL
|
|
|
static func isValidURL(_ url: String) -> Bool {
|
|
|
url.isValidURL
|
|
|
}
|
|
|
|
|
|
/// 验证必填字段
|
|
|
static func isRequiredFieldValid(_ field: String) -> Bool {
|
|
|
!field.isEmptyOrWhitespace
|
|
|
}
|
|
|
|
|
|
/// 验证字符长度
|
|
|
static func isLengthValid(_ text: String, min: Int, max: Int) -> Bool {
|
|
|
let count = text.characterCount
|
|
|
return count >= min && count <= max
|
|
|
}
|
|
|
|
|
|
/// 验证密码强度
|
|
|
static func getPasswordStrength(_ password: String) -> PasswordStrength {
|
|
|
var score = 0
|
|
|
|
|
|
if password.count >= 8 { score += 1 }
|
|
|
if password.range(of: "[a-z]", options: .regularExpression) != nil { score += 1 }
|
|
|
if password.range(of: "[A-Z]", options: .regularExpression) != nil { score += 1 }
|
|
|
if password.range(of: "[0-9]", options: .regularExpression) != nil { score += 1 }
|
|
|
if password.range(of: "[^a-zA-Z0-9]", options: .regularExpression) != nil { score += 1 }
|
|
|
|
|
|
switch score {
|
|
|
case 0...1:
|
|
|
return .weak
|
|
|
case 2...3:
|
|
|
return .medium
|
|
|
case 4...5:
|
|
|
return .strong
|
|
|
default:
|
|
|
return .weak
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 密码强度枚举
|
|
|
enum PasswordStrength {
|
|
|
case weak
|
|
|
case medium
|
|
|
case strong
|
|
|
|
|
|
var description: String {
|
|
|
switch self {
|
|
|
case .weak:
|
|
|
return "weak".localized
|
|
|
case .medium:
|
|
|
return "medium".localized
|
|
|
case .strong:
|
|
|
return "strong".localized
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var color: Color {
|
|
|
switch self {
|
|
|
case .weak:
|
|
|
return .red
|
|
|
case .medium:
|
|
|
return .orange
|
|
|
case .strong:
|
|
|
return .green
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 通用格式化函数
|
|
|
struct FormatHelper {
|
|
|
/// 格式化文件大小
|
|
|
static func formatFileSize(_ bytes: Int64) -> String {
|
|
|
let formatter = ByteCountFormatter()
|
|
|
formatter.allowedUnits = [.useKB, .useMB, .useGB]
|
|
|
formatter.countStyle = .file
|
|
|
return formatter.string(fromByteCount: bytes)
|
|
|
}
|
|
|
|
|
|
/// 格式化数字
|
|
|
static func formatNumber(_ number: Int) -> String {
|
|
|
let formatter = NumberFormatter()
|
|
|
formatter.numberStyle = .decimal
|
|
|
return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
|
|
|
}
|
|
|
|
|
|
/// 格式化百分比
|
|
|
static func formatPercentage(_ value: Double) -> String {
|
|
|
let formatter = NumberFormatter()
|
|
|
formatter.numberStyle = .percent
|
|
|
formatter.minimumFractionDigits = 1
|
|
|
formatter.maximumFractionDigits = 1
|
|
|
return formatter.string(from: NSNumber(value: value)) ?? "\(value * 100)%"
|
|
|
}
|
|
|
|
|
|
/// 格式化货币
|
|
|
static func formatCurrency(_ amount: Double, locale: Locale = .current) -> String {
|
|
|
let formatter = NumberFormatter()
|
|
|
formatter.numberStyle = .currency
|
|
|
formatter.locale = locale
|
|
|
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 通用动画函数
|
|
|
struct AnimationHelper {
|
|
|
/// 弹性动画
|
|
|
static let spring = Animation.spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0)
|
|
|
|
|
|
/// 缓入动画
|
|
|
static let easeIn = Animation.easeIn(duration: 0.3)
|
|
|
|
|
|
/// 缓出动画
|
|
|
static let easeOut = Animation.easeOut(duration: 0.3)
|
|
|
|
|
|
/// 缓入缓出动画
|
|
|
static let easeInOut = Animation.easeInOut(duration: 0.3)
|
|
|
|
|
|
/// 线性动画
|
|
|
static let linear = Animation.linear(duration: 0.3)
|
|
|
}
|
|
|
|
|
|
// MARK: - 通用反馈函数
|
|
|
struct FeedbackHelper {
|
|
|
/// 轻触反馈
|
|
|
static func lightImpact() {
|
|
|
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
|
|
|
impactFeedback.impactOccurred()
|
|
|
}
|
|
|
|
|
|
/// 中等触反馈
|
|
|
static func mediumImpact() {
|
|
|
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
|
|
|
impactFeedback.impactOccurred()
|
|
|
}
|
|
|
|
|
|
/// 重触反馈
|
|
|
static func heavyImpact() {
|
|
|
let impactFeedback = UIImpactFeedbackGenerator(style: .heavy)
|
|
|
impactFeedback.impactOccurred()
|
|
|
}
|
|
|
|
|
|
/// 成功反馈
|
|
|
static func success() {
|
|
|
let notificationFeedback = UINotificationFeedbackGenerator()
|
|
|
notificationFeedback.notificationOccurred(.success)
|
|
|
}
|
|
|
|
|
|
/// 警告反馈
|
|
|
static func warning() {
|
|
|
let notificationFeedback = UINotificationFeedbackGenerator()
|
|
|
notificationFeedback.notificationOccurred(.warning)
|
|
|
}
|
|
|
|
|
|
/// 错误反馈
|
|
|
static func error() {
|
|
|
let notificationFeedback = UINotificationFeedbackGenerator()
|
|
|
notificationFeedback.notificationOccurred(.error)
|
|
|
}
|
|
|
} |