import SwiftUI import EventKit struct ContentView: View { @StateObject private var calendarService = CalendarService() @State private var scrollToToday = false @State private var showingSettings = false var body: some View { VStack(spacing: 0) { if calendarService.hasAccess { ScrollViewReader { proxy in List { ForEach(groupedEventsByDay(), id: \.date) { dayGroup in Section(header: dayHeader(for: dayGroup.date)) { ForEach(dayGroup.events, id: \.eventIdentifier) { event in EventRow(event: event) } } .id(dayGroup.date) } } .listStyle(.plain) .onAppear { if !scrollToToday { let today = Calendar.current.startOfDay(for: Date()) proxy.scrollTo(today, anchor: .top) scrollToToday = true } } .onChange(of: scrollToToday) { _, newValue in if newValue { let today = Calendar.current.startOfDay(for: Date()) withAnimation { proxy.scrollTo(today, anchor: .top) } scrollToToday = false } } } } else { VStack { Text("カレンダーへのアクセス権限が必要です") .font(.headline) Text("システム設定からアクセスを許可してください") .font(.subheadline) .foregroundColor(.secondary) } .padding() } } .sheet(isPresented: $showingSettings) { SettingsView(calendarService: calendarService) } .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { showingSettings = true }) { Label("設定", systemImage: "gearshape") } } ToolbarItem(placement: .primaryAction) { Button(action: { scrollToToday = true }) { Label("今日", systemImage: "calendar") } } } } private func groupedEventsByDay() -> [DayGroup] { let calendar = Calendar.current let grouped = Dictionary(grouping: calendarService.events) { event in calendar.startOfDay(for: event.startDate) } let today = calendar.startOfDay(for: Date()) guard let startDate = calendar.date(byAdding: .day, value: -14, to: today), let endDate = calendar.date(byAdding: .day, value: 14, to: today) else { return [] } var allDates: [Date] = [] var currentDate = startDate while currentDate <= endDate { allDates.append(currentDate) guard let nextDate = calendar.date(byAdding: .day, value: 1, to: currentDate) else { break } currentDate = nextDate } return allDates.map { date in DayGroup(date: date, events: grouped[date] ?? []) }.sorted { $0.date < $1.date } } private func dayHeader(for date: Date) -> some View { let calendar = Calendar.current let isToday = calendar.isDateInToday(date) let formatter = DateFormatter() formatter.dateFormat = "M月d日(E)" formatter.locale = Locale(identifier: "ja_JP") return HStack { Text(formatter.string(from: date)) .font(.headline) .foregroundColor(isToday ? .blue : .primary) if isToday { Text("今日") .font(.caption) .padding(.horizontal, 6) .padding(.vertical, 2) .background(Color.blue) .foregroundColor(.white) .cornerRadius(4) } } } } struct DayGroup { let date: Date let events: [EKEvent] } struct EventRow: View { let event: EKEvent @State private var showingDetail = false var body: some View { Button(action: { showingDetail = true }) { VStack(alignment: .leading, spacing: 4) { HStack { if event.isAllDay { Text("終日") .font(.caption) .foregroundColor(.secondary) } else { Text(timeString(from: event.startDate, to: event.endDate)) .font(.caption) .foregroundColor(.secondary) } if let calendar = event.calendar { Circle() .fill(Color(calendar.color)) .frame(width: 8, height: 8) } } Text(event.title ?? "無題のイベント") .font(.body) if let location = event.location, !location.isEmpty { Text(location) .font(.caption) .foregroundColor(.secondary) } } .padding(.vertical, 4) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } .buttonStyle(.plain) .popover(isPresented: $showingDetail) { EventDetailView(event: event) } } private func timeString(from start: Date, to end: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "HH:mm" return "\(formatter.string(from: start)) - \(formatter.string(from: end))" } } struct EventDetailView: View { let event: EKEvent var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { if let calendar = event.calendar { Circle() .fill(Color(calendar.color)) .frame(width: 12, height: 12) Text(calendar.title) .font(.caption) .foregroundColor(.secondary) } } Text(event.title ?? "無題のイベント") .font(.headline) Divider() VStack(alignment: .leading, spacing: 8) { if event.isAllDay { HStack { Image(systemName: "clock") .foregroundColor(.secondary) .frame(width: 20) Text("終日") } } else { HStack(alignment: .top) { Image(systemName: "clock") .foregroundColor(.secondary) .frame(width: 20) VStack(alignment: .leading, spacing: 2) { Text(formatDateTime(event.startDate)) Text("〜") .foregroundColor(.secondary) .font(.caption) Text(formatDateTime(event.endDate)) } } } if let location = event.location, !location.isEmpty { HStack(alignment: .top) { Image(systemName: "location") .foregroundColor(.secondary) .frame(width: 20) Text(location) } } if let notes = event.notes, !notes.isEmpty { HStack(alignment: .top) { Image(systemName: "note.text") .foregroundColor(.secondary) .frame(width: 20) Text(notes) .textSelection(.enabled) } } if let url = event.url { HStack(alignment: .top) { Image(systemName: "link") .foregroundColor(.secondary) .frame(width: 20) Link(url.absoluteString, destination: url) } } } } .padding() .frame(minWidth: 300, maxWidth: 400) } private func formatDateTime(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "M月d日(E) HH:mm" formatter.locale = Locale(identifier: "ja_JP") return formatter.string(from: date) } }