refactor: notifications and settings view models; enhance login and registration UI
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user