Загрузка данных
import SwiftUI
struct HistoryView: View {
@ObservedObject var store: StickStore
@State private var logToDelete: StickLog? = nil
@State private var showDeleteAlert = false
var totalCount: Int {
store.logs.count
}
var firstLogDate: Date? {
store.logs.map { $0.date }.min()
}
var lastLogDate: Date? {
store.logs.map { $0.date }.max()
}
struct DayGroup: Identifiable {
var id: Date { date }
let date: Date
let logs: [StickLog]
}
struct MonthGroup: Identifiable {
var id: String { month }
let month: String
let days: [DayGroup]
}
struct YearGroup: Identifiable {
var id: String { year }
let year: String
let months: [MonthGroup]
}
var groupedByYearAndMonth: [YearGroup] {
let calendar = Calendar.current
let dailyGrouped = Dictionary(grouping: store.logs) { log in
calendar.startOfDay(for: log.date)
}
let daysArray = dailyGrouped.map { DayGroup(date: $0.key, logs: $0.value.sorted(by: { $0.date > $1.date })) }
let yearGrouped = Dictionary(grouping: daysArray) { day in
calendar.component(.year, from: day.date)
}
return yearGrouped.map { (yearKey, daysInYear) in
let monthGrouped = Dictionary(grouping: daysInYear) { day in
calendar.component(.month, from: day.date)
}
let monthsArray = monthGrouped.map { (monthKey, daysInMonth) in
let monthName = formattedMonthName(monthKey)
let sortedDays = daysInMonth.sorted(by: { $0.date > $1.date })
return MonthGroup(month: monthName, days: sortedDays)
}.sorted { m1, m2 in
guard let d1 = m1.days.first?.date, let d2 = m2.days.first?.date else { return false }
return d1 > d2
}
return YearGroup(year: String(yearKey), months: monthsArray)
}.sorted { y1, y2 in
(Int(y1.year) ?? 0) > (Int(y2.year) ?? 0)
}
}
var body: some View {
ZStack(alignment: .top) {
// Задний фон
Color(uiColor: .systemGroupedBackground)
.ignoresSafeArea()
// Основной скролл с отступом сверху под плашку
ScrollView {
VStack(spacing: 20) {
topCardsView
if store.logs.isEmpty {
emptyStateView
} else {
historyListView
}
}
.padding(.top, 70) // Отступ, чтобы контент не перекрывался заголовком изначально
.padding(.bottom, 20)
}
// Нативная блюр-панель сверху
blurHeaderView
}
.alert("Удалить эту запись?", isPresented: $showDeleteAlert) {
Button("Удалить", role: .destructive) {
if let log = logToDelete {
let impact = UINotificationFeedbackGenerator()
impact.notificationOccurred(.error)
store.deleteLog(log)
}
}
Button("Отмена", role: .cancel) {
let impact = UIImpactFeedbackGenerator(style: .light)
impact.impactOccurred()
logToDelete = nil
}
} message: {
Text("Запись будет удалена безвозвратно.")
}
}
// MARK: - Subviews
private var blurHeaderView: some View {
VStack(spacing: 0) {
HStack {
Text("Вся история")
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundColor(.primary)
Spacer()
}
.padding(.horizontal)
.padding(.bottom, 12)
}
.frame(maxWidth: .infinity)
.background(.ultraThinMaterial) // Красивый блюр с эффектом размытия контента
.overlay(
Divider()
.opacity(0.4),
alignment: .bottom
)
}
private var topCardsView: some View {
VStack(spacing: 12) {
// Карточка общего количества
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("ВСЕГО СКУРЕНО")
.font(.system(size: 11, weight: .black))
.foregroundColor(.primary.opacity(0.6))
.tracking(1.0)
Text("\(totalCount) шт.")
.font(.system(size: 28, weight: .heavy, design: .rounded))
.foregroundColor(.orange)
}
Spacer()
Image(systemName: "flame.circle.fill")
.font(.system(size: 38))
.foregroundColor(.orange)
}
.padding(.vertical, 16)
.padding(.horizontal, 20)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.cornerRadius(18)
.shadow(color: Color.black.opacity(0.04), radius: 6, x: 0, y: 3)
// Карточки первого и последнего стика
HStack(spacing: 14) {
VStack(spacing: 6) {
Text("ПЕРВАЯ ЗАПИСЬ")
.font(.system(size: 10, weight: .black))
.foregroundColor(.orange)
.tracking(1.0)
if let firstDate = firstLogDate {
Text(firstDate, style: .date)
.font(.system(size: 14, weight: .bold))
.multilineTextAlignment(.center)
} else {
Text("—")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.padding(.horizontal, 10)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.cornerRadius(18)
.shadow(color: Color.black.opacity(0.04), radius: 6, x: 0, y: 3)
VStack(spacing: 6) {
Text("ПОСЛЕДНЯЯ ЗАПИСЬ")
.font(.system(size: 10, weight: .black))
.foregroundColor(.red)
.tracking(1.0)
if let lastDate = lastLogDate {
Text(lastDate, style: .date)
.font(.system(size: 14, weight: .bold))
.multilineTextAlignment(.center)
} else {
Text("—")
.font(.system(size: 14, weight: .bold))
.foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.padding(.horizontal, 10)
.background(Color(uiColor: .secondarySystemGroupedBackground))
.cornerRadius(18)
.shadow(color: Color.black.opacity(0.04), radius: 6, x: 0, y: 3)
}
}
.padding(.horizontal)
}
private var emptyStateView: some View {
VStack(spacing: 12) {
Image(systemName: "tray")
.font(.system(size: 48))
.foregroundColor(.secondary)
Text("История пуста")
.font(.headline)
Text("Здесь будут отображаться абсолютно все ваши записи.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding(.top, 40)
.padding(.horizontal)
}
private var historyListView: some View {
LazyVStack(alignment: .leading, spacing: 24) {
ForEach(groupedByYearAndMonth) { yearGroup in
VStack(alignment: .leading, spacing: 16) {
Text(yearGroup.year)
.font(.system(size: 28, weight: .heavy, design: .rounded))
.padding(.horizontal)
ForEach(yearGroup.months) { monthGroup in
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(monthGroup.month.uppercased())
.font(.system(size: 13, weight: .bold))
.foregroundColor(.primary)
.tracking(1.2)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background(Color.orange.opacity(0.15))
.cornerRadius(12)
.padding(.horizontal)
ForEach(monthGroup.days) { dayGroup in
dayCell(dayGroup)
}
}
}
}
}
}
}
private func dayCell(_ dayGroup: DayGroup) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(formattedDayHeader(dayGroup.date))
.font(.system(size: 14, weight: .semibold))
.foregroundColor(.secondary)
.padding(.horizontal)
VStack(spacing: 0) {
ForEach(Array(dayGroup.logs.enumerated()), id: \.element.id) { index, log in
HStack {
Image(systemName: "flame.fill")
.font(.system(size: 16))
.foregroundColor(.orange)
Text(log.date, style: .time)
.font(.system(size: 16, weight: .medium))
Spacer()
Button(action: {
let impact = UINotificationFeedbackGenerator()
impact.notificationOccurred(.warning)
logToDelete = log
showDeleteAlert = true
}) {
Image(systemName: "trash")
.font(.system(size: 14))
.foregroundColor(.red.opacity(0.7))
.padding(8)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
if index < dayGroup.logs.count - 1 {
Divider()
.padding(.leading, 16)
}
}
}
.background(Color(uiColor: .secondarySystemGroupedBackground))
.cornerRadius(16)
.padding(.horizontal)
}
}
private func formattedMonthName(_ monthNumber: Int) -> String {
let fmt = DateFormatter()
fmt.locale = Locale(identifier: "ru_RU")
return fmt.standaloneMonthSymbols[monthNumber - 1].capitalized
}
private func formattedDayHeader(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ru_RU")
formatter.dateFormat = "d MMMM, EEEE"
return formatter.string(from: date).capitalized
}
}