refactor: notifications and settings view models; enhance login and registration UI

This commit is contained in:
2026-03-15 21:40:20 +07:00
parent 0947c048c1
commit 37b87ececd
45 changed files with 985 additions and 680 deletions
+20
View File
@@ -0,0 +1,20 @@
import SwiftUI
struct AppBackgroundModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(
LinearGradient(
colors: [Color(.systemGroupedBackground), Color.red.opacity(0.08)],
startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
)
}
}
extension View {
func appBackground() -> some View {
modifier(AppBackgroundModifier())
}
}
+25
View File
@@ -0,0 +1,25 @@
import SwiftUI
struct AppSecureField: View {
let title: LocalizedStringKey
let icon: String
@Binding var text: String
var body: some View {
HStack(spacing: 10) {
Image(systemName: icon)
.foregroundStyle(.secondary)
.frame(width: 18)
SecureField(title, text: $text)
}
.padding(.horizontal, 14)
.padding(.vertical, 14)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(Color.primary.opacity(0.08), lineWidth: 1)
)
}
}
+25
View File
@@ -0,0 +1,25 @@
import SwiftUI
struct AppTextField: View {
let title: LocalizedStringKey
let icon: String
@Binding var text: String
var body: some View {
HStack(spacing: 10) {
Image(systemName: icon)
.foregroundStyle(.secondary)
.frame(width: 18)
TextField(title, text: $text)
}
.padding(.horizontal, 14)
.padding(.vertical, 14)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(Color.primary.opacity(0.08), lineWidth: 1)
)
}
}
+22
View File
@@ -0,0 +1,22 @@
import SwiftUI
struct CardContainerModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding(.horizontal, 20)
.padding(.vertical, 24)
.background(Color(.systemBackground).opacity(0.8))
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 28, style: .continuous)
.stroke(Color.primary.opacity(0.06), lineWidth: 1)
)
.padding(16)
}
}
extension View {
func cardContainer() -> some View {
modifier(CardContainerModifier())
}
}
@@ -0,0 +1,47 @@
import SwiftUI
enum NotificationSeverity: String {
case critical
case warning
case info
case success
var icon: String {
switch self {
case .critical: return "exclamationmark.triangle.fill"
case .warning: return "exclamationmark.circle.fill"
case .info: return "info.circle.fill"
case .success: return "checkmark.seal.fill"
}
}
var color: Color {
switch self {
case .critical: return .red
case .warning: return .orange
case .info: return .blue
case .success: return .green
}
}
init(from metadata: [String: String]?) {
let raw = metadata?["severity"]?.lowercased() ?? ""
self = NotificationSeverity(rawValue: raw) ?? .info
}
}
struct NotificationIconView: View {
let severity: NotificationSeverity
let isActive: Bool
var body: some View {
ZStack {
Circle()
.fill(isActive ? .white.opacity(0.25) : severity.color.opacity(0.12))
.frame(width: 40, height: 40)
Image(systemName: severity.icon)
.font(.body)
.foregroundStyle(isActive ? .white : severity.color)
}
}
}
+100
View File
@@ -0,0 +1,100 @@
import SwiftUI
import UIKit
struct OTPDigitField: UIViewRepresentable {
@Binding var text: String
let isFocused: Bool
let onFocus: () -> Void
let onInsert: () -> Void
let onDeleteWhenEmpty: () -> Void
let onPaste: ([String]) -> Void
func makeUIView(context: Context) -> BackspaceAwareTextField {
let textField = BackspaceAwareTextField()
textField.delegate = context.coordinator
textField.keyboardType = .numberPad
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 24, weight: .bold)
textField.textContentType = .oneTimeCode
textField.onDeleteWhenEmpty = {
onDeleteWhenEmpty()
}
textField.addTarget(context.coordinator, action: #selector(Coordinator.editingChanged(_:)), for: .editingChanged)
return textField
}
func updateUIView(_ uiView: BackspaceAwareTextField, context: Context) {
if uiView.text != text {
uiView.text = text
}
if isFocused && !uiView.isFirstResponder {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
var parent: OTPDigitField
init(parent: OTPDigitField) {
self.parent = parent
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
parent.onFocus()
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.onFocus()
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.isEmpty {
return true
}
let digits = string.filter { $0.isNumber }
guard !digits.isEmpty else {
return false
}
if digits.count > 1 {
parent.onPaste(digits.map(String.init))
return false
}
parent.text = String(digits.prefix(1))
parent.onInsert()
return false
}
@objc
func editingChanged(_ textField: UITextField) {
let digitsOnly = (textField.text ?? "").filter { $0.isNumber }
let single = String(digitsOnly.prefix(1))
if textField.text != single {
textField.text = single
}
parent.text = single
}
}
}
final class BackspaceAwareTextField: UITextField {
var onDeleteWhenEmpty: (() -> Void)?
override func deleteBackward() {
let wasEmpty = (text ?? "").isEmpty
super.deleteBackward()
if wasEmpty {
onDeleteWhenEmpty?()
}
}
}